Skip to content

Commit 95ef1cf

Browse files
authored
Merge pull request #330 from cicirello/cache-hashcode
Cache hash on first call to Permutation.hashCode()
2 parents c71701a + 0c2d3c2 commit 95ef1cf

16 files changed

+233
-58
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7-
## [Unreleased] - 2023-04-07
7+
## [Unreleased] - 2023-04-13
88

99
### Added
1010

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

1314
### Deprecated
1415

src/main/java/org/cicirello/permutations/Permutation.java

Lines changed: 72 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,27 @@
4242
public final class Permutation
4343
implements Serializable, Iterable<Permutation>, Copyable<Permutation> {
4444

45-
private static final long serialVersionUID = 1L;
45+
private static final long serialVersionUID = 2L;
4646

4747
/**
4848
* Raw permutation, which should consist of a permutation of the integers in [0,
4949
* permutation.length).
5050
*/
5151
private final int[] permutation;
5252

53+
/*
54+
* Class caches the hashCode the first time hashCode() is called
55+
* to avoid cost of recomputing in applications that rely on HashSets or HashMaps
56+
* of Permutations, etc with heavy use of the hashCode.
57+
*/
58+
private transient int hashCode;
59+
60+
/*
61+
* Flag for validating/invalidating cache of hashCode. All methods
62+
* that change state of Permutation must invalidate the cache.
63+
*/
64+
private transient boolean hashCodeIsCached;
65+
5366
/**
5467
* Initializes a random permutation of n integers. Uses {@link ThreadLocalRandom} as the source of
5568
* efficient random number generation.
@@ -159,6 +172,8 @@ private Permutation(int[] p, boolean validate) {
159172
*/
160173
public Permutation(Permutation p) {
161174
permutation = p.permutation.clone();
175+
hashCodeIsCached = p.hashCodeIsCached;
176+
hashCode = p.hashCode;
162177
}
163178

164179
/**
@@ -197,6 +212,7 @@ public Permutation(Permutation p, int length) {
197212
*/
198213
public void apply(PermutationUnaryOperator operator) {
199214
operator.apply(permutation);
215+
hashCodeIsCached = false;
200216
}
201217

202218
/**
@@ -206,6 +222,7 @@ public void apply(PermutationUnaryOperator operator) {
206222
*/
207223
public void apply(PermutationFullUnaryOperator operator) {
208224
operator.apply(permutation, this);
225+
hashCodeIsCached = false;
209226
}
210227

211228
/**
@@ -218,6 +235,8 @@ public void apply(PermutationFullUnaryOperator operator) {
218235
*/
219236
public void apply(PermutationBinaryOperator operator, Permutation other) {
220237
operator.apply(permutation, other.permutation);
238+
hashCodeIsCached = false;
239+
other.hashCodeIsCached = false;
221240
}
222241

223242
/**
@@ -230,6 +249,8 @@ public void apply(PermutationBinaryOperator operator, Permutation other) {
230249
*/
231250
public void apply(PermutationFullBinaryOperator operator, Permutation other) {
232251
operator.apply(permutation, other.permutation, this, other);
252+
hashCodeIsCached = false;
253+
other.hashCodeIsCached = false;
233254
}
234255

235256
/**
@@ -243,6 +264,7 @@ public void apply(PermutationFullBinaryOperator operator, Permutation other) {
243264
public void applyThenValidate(PermutationUnaryOperator operator) {
244265
try {
245266
operator.apply(permutation);
267+
hashCodeIsCached = false;
246268
validate(permutation);
247269
} catch (IllegalArgumentException exception) {
248270
throw new IllegalPermutationStateException(
@@ -261,6 +283,7 @@ public void applyThenValidate(PermutationUnaryOperator operator) {
261283
public void applyThenValidate(PermutationFullUnaryOperator operator) {
262284
try {
263285
operator.apply(permutation, this);
286+
hashCodeIsCached = false;
264287
validate(permutation);
265288
} catch (IllegalArgumentException exception) {
266289
throw new IllegalPermutationStateException(
@@ -282,6 +305,8 @@ public void applyThenValidate(PermutationFullUnaryOperator operator) {
282305
public void applyThenValidate(PermutationBinaryOperator operator, Permutation other) {
283306
try {
284307
operator.apply(permutation, other.permutation);
308+
hashCodeIsCached = false;
309+
other.hashCodeIsCached = false;
285310
validate(permutation);
286311
validate(other.permutation);
287312
} catch (IllegalArgumentException exception) {
@@ -305,6 +330,8 @@ public void applyThenValidate(PermutationBinaryOperator operator, Permutation ot
305330
public void applyThenValidate(PermutationFullBinaryOperator operator, Permutation other) {
306331
try {
307332
operator.apply(permutation, other.permutation, this, other);
333+
hashCodeIsCached = false;
334+
other.hashCodeIsCached = false;
308335
validate(permutation);
309336
validate(other.permutation);
310337
} catch (IllegalArgumentException exception) {
@@ -413,6 +440,7 @@ public Permutation getInversePermutation() {
413440
*/
414441
public void invert() {
415442
System.arraycopy(getInverse(), 0, permutation, 0, permutation.length);
443+
hashCodeIsCached = false;
416444
}
417445

418446
/**
@@ -444,6 +472,7 @@ public void scramble(RandomGenerator r) {
444472
permutation[j] = i;
445473
}
446474
}
475+
hashCodeIsCached = false;
447476
}
448477
}
449478

@@ -471,13 +500,14 @@ public void scramble(RandomGenerator r, boolean guaranteeDifferent) {
471500
for (int i = permutation.length - 1; i > 1; i--) {
472501
int j = RandomIndexer.nextInt(i + 1, r);
473502
if (i != j) {
474-
swap(i, j);
503+
internalSwap(i, j);
475504
changed = true;
476505
}
477506
}
478507
if (permutation.length > 1 && (!changed || r.nextBoolean())) {
479-
swap(0, 1);
508+
internalSwap(0, 1);
480509
}
510+
hashCodeIsCached = false;
481511
} else {
482512
scramble(r);
483513
}
@@ -509,22 +539,23 @@ public void scramble(int i, int j, RandomGenerator r) {
509539
if (i == j) {
510540
return;
511541
}
542+
int k = j;
512543
if (i > j) {
513-
int temp = i;
544+
k = i;
514545
i = j;
515-
j = temp;
516546
}
517547
boolean changed = false;
518-
for (int k = j; k > i + 1; k--) {
548+
for (; k > i + 1; k--) {
519549
int l = i + RandomIndexer.nextInt(k - i + 1, r);
520550
if (l != k) {
521-
swap(l, k);
551+
internalSwap(l, k);
522552
changed = true;
523553
}
524554
}
525555
if (!changed || r.nextBoolean()) {
526-
swap(i, i + 1);
556+
internalSwap(i, i + 1);
527557
}
558+
hashCodeIsCached = false;
528559
}
529560

530561
/**
@@ -544,13 +575,14 @@ public void scramble(int[] indexes, RandomGenerator r) {
544575
for (int j = indexes.length - 1; j > 1; j--) {
545576
int i = RandomIndexer.nextInt(j + 1, r);
546577
if (i != j) {
547-
swap(indexes[i], indexes[j]);
578+
internalSwap(indexes[i], indexes[j]);
548579
changed = true;
549580
}
550581
}
551582
if (!changed || r.nextBoolean()) {
552-
swap(indexes[0], indexes[1]);
583+
internalSwap(indexes[0], indexes[1]);
553584
}
585+
hashCodeIsCached = false;
554586
}
555587
}
556588

@@ -667,6 +699,7 @@ public void swap(int i, int j) {
667699
int temp = permutation[i];
668700
permutation[i] = permutation[j];
669701
permutation[j] = temp;
702+
hashCodeIsCached = false;
670703
}
671704

672705
/**
@@ -688,6 +721,7 @@ public void cycle(int[] indexes) {
688721
permutation[indexes[i - 1]] = permutation[indexes[i]];
689722
}
690723
permutation[indexes[indexes.length - 1]] = temp;
724+
hashCodeIsCached = false;
691725
}
692726
}
693727

@@ -718,14 +752,16 @@ public void swapBlocks(int a, int b, int i, int j) {
718752
System.arraycopy(permutation, b + 1, temp, k, m);
719753
System.arraycopy(permutation, a, temp, k + m, b - a + 1);
720754
System.arraycopy(temp, 0, permutation, a, temp.length);
755+
hashCodeIsCached = false;
721756
}
722757
}
723758

724759
/** Reverses the order of the elements in the permutation. */
725760
public void reverse() {
726761
for (int i = 0, j = permutation.length - 1; i < j; i++, j--) {
727-
swap(i, j);
762+
internalSwap(i, j);
728763
}
764+
hashCodeIsCached = false;
729765
}
730766

731767
/**
@@ -739,13 +775,14 @@ public void reverse() {
739775
public void reverse(int i, int j) {
740776
if (i > j) {
741777
for (; i > j; i--, j++) {
742-
swap(i, j);
778+
internalSwap(i, j);
743779
}
744780
} else {
745781
for (; i < j; i++, j--) {
746-
swap(i, j);
782+
internalSwap(i, j);
747783
}
748784
}
785+
hashCodeIsCached = false;
749786
}
750787

751788
/**
@@ -762,10 +799,12 @@ public void removeAndInsert(int i, int j) {
762799
int n = permutation[i];
763800
System.arraycopy(permutation, i + 1, permutation, i, j - i);
764801
permutation[j] = n;
802+
hashCodeIsCached = false;
765803
} else if (i > j) {
766804
int n = permutation[i];
767805
System.arraycopy(permutation, j, permutation, j + 1, i - j);
768806
permutation[j] = n;
807+
hashCodeIsCached = false;
769808
}
770809
}
771810

@@ -784,6 +823,7 @@ public void rotate(int numPositions) {
784823
System.arraycopy(
785824
permutation, numPositions, permutation, 0, permutation.length - numPositions);
786825
System.arraycopy(temp, 0, permutation, permutation.length - numPositions, numPositions);
826+
hashCodeIsCached = false;
787827
}
788828
}
789829

@@ -809,11 +849,13 @@ public void removeAndInsert(int i, int size, int j) {
809849
System.arraycopy(permutation, j, temp, 0, i - j);
810850
System.arraycopy(permutation, i, permutation, j, size);
811851
System.arraycopy(temp, 0, permutation, j + size, i - j);
852+
hashCodeIsCached = false;
812853
} else { // Condition is implied by above: if (i < j)
813854
int[] temp = new int[size];
814855
System.arraycopy(permutation, i, temp, 0, size);
815856
System.arraycopy(permutation, i + size, permutation, i, j - i);
816857
System.arraycopy(temp, 0, permutation, j, size);
858+
hashCodeIsCached = false;
817859
}
818860
}
819861

@@ -831,6 +873,7 @@ public void set(int[] p) {
831873
}
832874
validate(p);
833875
System.arraycopy(p, 0, permutation, 0, p.length);
876+
hashCodeIsCached = false;
834877
}
835878

836879
/**
@@ -892,7 +935,11 @@ public boolean equals(Object other) {
892935
*/
893936
@Override
894937
public int hashCode() {
895-
return Arrays.hashCode(permutation);
938+
if (hashCodeIsCached) {
939+
return hashCode;
940+
}
941+
hashCodeIsCached = true;
942+
return hashCode = Arrays.hashCode(permutation);
896943
}
897944

898945
private boolean validate(int[] p) {
@@ -909,4 +956,15 @@ private boolean validate(int[] p) {
909956
}
910957
return true;
911958
}
959+
960+
/*
961+
* Use internally, such as from reverse, etc to avoid
962+
* repeatedly invalidating hashCode cache (as well as from
963+
* the PermutationIterator).
964+
*/
965+
final void internalSwap(int i, int j) {
966+
int temp = permutation[i];
967+
permutation[i] = permutation[j];
968+
permutation[j] = temp;
969+
}
912970
}

src/main/java/org/cicirello/permutations/PermutationIterator.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@
3939
*/
4040
public class PermutationIterator implements Iterator<Permutation> {
4141

42-
private Permutation p;
43-
private int[] lastSwap;
42+
private final Permutation p;
43+
private final int[] lastSwap;
4444
private boolean done;
4545

4646
/**
@@ -92,14 +92,14 @@ public Permutation next() {
9292
done = true;
9393
} else {
9494
for (int i = lastSwap.length - 2; i >= 0; i--) {
95-
if (lastSwap[i] != i) p.swap(i, lastSwap[i]);
95+
if (lastSwap[i] != i) p.internalSwap(i, lastSwap[i]);
9696
if (lastSwap[i] == lastSwap.length - 1) {
9797
lastSwap[i] = i;
9898
if (i == 0) done = true;
9999
continue;
100100
}
101101
lastSwap[i]++;
102-
p.swap(i, lastSwap[i]);
102+
p.internalSwap(i, lastSwap[i]);
103103
break;
104104
}
105105
}

src/test/java/org/cicirello/permutations/PermutationConstructorRelatedTests.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
* JavaPermutationTools: A Java library for computation on permutations and sequences
3-
* Copyright 2005-2022 Vincent A. Cicirello, <https://www.cicirello.org/>.
3+
* Copyright 2005-2023 Vincent A. Cicirello, <https://www.cicirello.org/>.
44
*
55
* This file is part of JavaPermutationTools (https://jpt.cicirello.org/).
66
*
@@ -155,6 +155,7 @@ public void testPermutationCopyConstructor() {
155155
Permutation copy = new Permutation(p);
156156
assertEquals(p, copy);
157157
assertEquals(p.hashCode(), copy.hashCode());
158+
assertEquals(p.hashCode(), copy.hashCode());
158159
}
159160
}
160161
}
@@ -168,6 +169,7 @@ public void testPermutationCopyMethod() {
168169
assertEquals(p, copy);
169170
assertTrue(p != copy);
170171
assertEquals(p.hashCode(), copy.hashCode());
172+
assertEquals(p.hashCode(), copy.hashCode());
171173
}
172174
}
173175
}

0 commit comments

Comments
 (0)