Initial commit of split off of Utilities project

master
Robinson 2022-03-19 17:07:21 +01:00
commit a4c33b53d5
No known key found for this signature in database
GPG Key ID: 8E7DB78588BD6F5C
57 changed files with 19862 additions and 0 deletions

121
.gitignore vendored Normal file
View File

@ -0,0 +1,121 @@
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
.idea/**/codeStyles/
.idea/**/codeStyleSettings.xml
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/shelf/
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# CMake
cmake-build-debug/
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
######################
# End JetBrains IDEs #
######################
# From https://github.com/github/gitignore/blob/master/Gradle.gitignore
.gradle
/build/
# Ignore Gradle GUI config
gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
!gradle-wrapper.properties
# Cache of project
.gradletasknamecache
# From https://github.com/github/gitignore/blob/master/Java.gitignore
*.class
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
*.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
##########################################################
# Specific to this module
# iml files are generated by intellij/gradle now
**/*.iml

29
LICENSE Normal file
View File

@ -0,0 +1,29 @@
- OS - Information about the system, Java runtime, OS, Window Manager, and Desktop Environment.
[The Apache Software License, Version 2.0]
https://git.dorkbox.com/dorkbox/OS
Copyright 2022
Dorkbox LLC
Extra license information
- Kotlin -
[The Apache Software License, Version 2.0]
https://github.com/JetBrains/kotlin
Copyright 2020
JetBrains s.r.o. and Kotlin Programming Language contributors
Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply
See: https://github.com/JetBrains/kotlin/blob/master/license/README.md
- Updates - Software Update Management
[The Apache Software License, Version 2.0]
https://git.dorkbox.com/dorkbox/Updates
Copyright 2021
Dorkbox LLC
Extra license information
- Kotlin -
[The Apache Software License, Version 2.0]
https://github.com/JetBrains/kotlin
Copyright 2020
JetBrains s.r.o. and Kotlin Programming Language contributors
Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply
See: https://github.com/JetBrains/kotlin/blob/master/license/README.md

218
LICENSE.Apachev2 Normal file
View File

@ -0,0 +1,218 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

39
README.md Executable file
View File

@ -0,0 +1,39 @@
Niche collections to augment what is already available.
###### [![Dorkbox](https://badge.dorkbox.com/dorkbox.svg "Dorkbox")](https://git.dorkbox.com/dorkbox/Collections) [![Github](https://badge.dorkbox.com/github.svg "Github")](https://github.com/dorkbox/Collections) [![Gitlab](https://badge.dorkbox.com/gitlab.svg "Gitlab")](https://gitlab.com/dorkbox/Collections)
* LockFree, performant collections/maps/sets/bi-maps
* Thread-safe concurrent iterators
* AhoCorasick finite state machine
* Performant, Primative Maps (int/long/float)
* TimSort
Maven Info
---------
```
<dependencies>
...
<dependency>
<groupId>com.dorkbox</groupId>
<artifactId>Collections</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
```
Gradle Info
---------
```
dependencies {
...
implementation("com.dorkbox:Collections:1.0")
}
```
License
---------
This project is © 2022 dorkbox llc, and is distributed under the terms of the Apache v2.0 License. See file "LICENSE" for further
references.

168
build.gradle.kts Normal file
View File

@ -0,0 +1,168 @@
/*
* Copyright 2020 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.time.Instant
///////////////////////////////
////// PUBLISH TO SONATYPE / MAVEN CENTRAL
////// TESTING : (to local maven repo) <'publish and release' - 'publishToMavenLocal'>
////// RELEASE : (to sonatype/maven central), <'publish and release' - 'publishToSonatypeAndRelease'>
///////////////////////////////
gradle.startParameter.showStacktrace = ShowStacktrace.ALWAYS // always show the stacktrace!
plugins {
id("com.dorkbox.GradleUtils") version "2.16"
id("com.dorkbox.Licensing") version "2.12"
id("com.dorkbox.VersionUpdate") version "2.4"
id("com.dorkbox.GradlePublish") version "1.12"
kotlin("jvm") version "1.6.10"
}
object Extras {
// set for the project
const val description = "Information about the system, Java runtime, OS, Window Manager, and Desktop Environment."
const val group = "com.dorkbox"
const val version = "1.0"
// set as project.ext
const val name = "Collections"
const val id = "Collections" // this is the maven ID!
const val vendor = "Dorkbox LLC"
const val vendorUrl = "https://dorkbox.com"
const val url = "https://git.dorkbox.com/dorkbox/Collections"
val buildDate = Instant.now().toString()
}
///////////////////////////////
///// assign 'Extras'
///////////////////////////////
GradleUtils.load("$projectDir/../../gradle.properties", Extras)
GradleUtils.defaults()
GradleUtils.compileConfiguration(JavaVersion.VERSION_1_8)
GradleUtils.jpms(JavaVersion.VERSION_1_9)
licensing {
license(License.APACHE_2) {
description(Extras.description)
author(Extras.vendor)
url(Extras.url)
extra("AhoCorasickDoubleArrayTrie", License.APACHE_2) {
description(Extras.description)
copyright(2018)
author("hankcs <me@hankcs.com>")
url("https://github.com/hankcs/AhoCorasickDoubleArrayTrie")
}
extra("Bias, BinarySearch", License.MIT) {
url(Extras.url)
url("https://github.com/timboudreau/util")
copyright(2013)
author("Tim Boudreau")
}
extra("ConcurrentEntry", License.APACHE_2) {
url(Extras.url)
copyright(2016)
author("bennidi")
author("dorkbox")
}
extra("Collection Utilities (Array, ArrayMap, BooleanArray, ByteArray, CharArray, FloatArray, IdentityMap, IntArray, IntFloatMap, IntIntMap, IntMap, IntSet, LongArray, LongMap, ObjectFloatMap, ObjectIntMap, ObjectMap, ObjectSet, OrderedMap, OrderedSet)", License.APACHE_2) {
url(Extras.url)
url("https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/utils")
copyright(2011)
author("LibGDX")
author("Mario Zechner (badlogicgames@gmail.com)")
author("Nathan Sweet (nathan.sweet@gmail.com)")
}
extra("Predicate", License.APACHE_2) {
url(Extras.url)
url("https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/utils")
copyright(2011)
author("LibGDX")
author("Mario Zechner (badlogicgames@gmail.com)")
author("Nathan Sweet (nathan.sweet@gmail.com)")
author("xoppa")
}
extra("Select, QuickSelect", License.APACHE_2) {
url(Extras.url)
url("https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/utils")
copyright(2011)
author("LibGDX")
author("Mario Zechner (badlogicgames@gmail.com)")
author("Nathan Sweet (nathan.sweet@gmail.com)")
author("Jon Renner")
}
extra("TimSort, ComparableTimSort", License.APACHE_2) {
url(Extras.url)
url("https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/utils")
copyright(2008)
author("The Android Open Source Project")
}
extra("ConcurrentWeakIdentityHashMap", License.APACHE_2) {
copyright(2016)
description("Concurrent WeakIdentity HashMap")
author("zhanhb")
url("https://github.com/spring-projects/spring-loaded/blob/master/springloaded/src/main/java/org/springsource/loaded/support/ConcurrentWeakIdentityHashMap.java")
}
}
}
tasks.jar.get().apply {
manifest {
// https://docs.oracle.com/javase/tutorial/deployment/jar/packageman.html
attributes["Name"] = Extras.name
attributes["Specification-Title"] = Extras.name
attributes["Specification-Version"] = Extras.version
attributes["Specification-Vendor"] = Extras.vendor
attributes["Implementation-Title"] = "${Extras.group}.${Extras.id}"
attributes["Implementation-Version"] = Extras.buildDate
attributes["Implementation-Vendor"] = Extras.vendor
}
}
dependencies {
api("com.dorkbox:Updates:1.1")
}
publishToSonatype {
groupId = Extras.group
artifactId = Extras.id
version = Extras.version
name = Extras.name
description = Extras.description
url = Extras.url
vendor = Extras.vendor
vendorUrl = Extras.vendorUrl
issueManagement {
url = "${Extras.url}/issues"
nickname = "Gitea Issues"
}
developer {
id = "dorkbox"
name = Extras.vendor
email = "email@dorkbox.com"
}
}

15
gradle.properties Normal file
View File

@ -0,0 +1,15 @@
# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties
org.gradle.jvmargs=-Dfile.encoding=UTF-8
#org.gradle.warning.mode=(all,fail,none,summary)
org.gradle.warning.mode=all
#org.gradle.daemon=false
# default is 3 hours, this is 1 minute
org.gradle.daemon.idletimeout=60000
#org.gradle.console=(auto,plain,rich,verbose)
org.gradle.console=auto
#org.gradle.logging.level=(quiet,warn,lifecycle,info,debug)
org.gradle.logging.level=lifecycle

15
settings.gradle.kts Normal file
View File

@ -0,0 +1,15 @@
/*
* Copyright 2018 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

View File

@ -0,0 +1,662 @@
/*******************************************************************************
* Copyright 2011 LibGDX.
* Mario Zechner <badlogicgames@gmail.com>
* Nathan Sweet <nathan.sweet@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package dorkbox.collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.NoSuchElementException;
/** A resizable, ordered or unordered array of objects. If unordered, this class avoids a memory copy when removing elements (the
* last element is moved to the removed element's position).
* @author Nathan Sweet */
@SuppressWarnings({"unchecked", "rawtypes", "SuspiciousSystemArraycopy", "unused", "NullableProblems", "DuplicatedCode"})
public class Array<T> implements Iterable<T> {
/** Provides direct access to the underlying array. If the Array's generic type is not Object, this field may only be accessed
* if the {@link Array#Array(boolean, int, Class)} constructor was used. */
public T[] items;
public int size;
public boolean ordered;
private ArrayIterable iterable;
private Predicate.PredicateIterable<T> predicateIterable;
/** Creates an ordered array with a capacity of 16. */
public Array () {
this(true, 16);
}
/** Creates an ordered array with the specified capacity. */
public Array (int capacity) {
this(true, capacity);
}
/** @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a
* memory copy.
* @param capacity Any elements added beyond this will cause the backing array to be grown. */
public Array (boolean ordered, int capacity) {
this.ordered = ordered;
items = (T[])new Object[capacity];
}
/** Creates a new array with {@link #items} of the specified type.
* @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a
* memory copy.
* @param capacity Any elements added beyond this will cause the backing array to be grown. */
public Array (boolean ordered, int capacity, Class arrayType) {
this.ordered = ordered;
items = (T[])java.lang.reflect.Array.newInstance(arrayType, capacity);
}
/** Creates an ordered array with {@link #items} of the specified type and a capacity of 16. */
public Array (Class arrayType) {
this(true, 16, arrayType);
}
/** Creates a new array containing the elements in the specified array. The new array will have the same type of backing array
* and will be ordered if the specified array is ordered. The capacity is set to the number of elements, so any subsequent
* elements added will cause the backing array to be grown. */
public Array (Array<? extends T> array) {
this(array.ordered, array.size, array.items.getClass().getComponentType());
size = array.size;
System.arraycopy(array.items, 0, items, 0, size);
}
/** Creates a new ordered array containing the elements in the specified array. The new array will have the same type of
* backing array. The capacity is set to the number of elements, so any subsequent elements added will cause the backing array
* to be grown. */
public Array (T[] array) {
this(true, array, 0, array.length);
}
/** Creates a new array containing the elements in the specified array. The new array will have the same type of backing array.
* The capacity is set to the number of elements, so any subsequent elements added will cause the backing array to be grown.
* @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a
* memory copy. */
public Array (boolean ordered, T[] array, int start, int count) {
this(ordered, count, (Class)array.getClass().getComponentType());
size = count;
System.arraycopy(array, start, items, 0, size);
}
public void add (T value) {
T[] items = this.items;
if (size == items.length) items = resize(Math.max(8, (int)(size * 1.75f)));
items[size++] = value;
}
public void add (T value1, T value2) {
T[] items = this.items;
if (size + 1 >= items.length) items = resize(Math.max(8, (int)(size * 1.75f)));
items[size] = value1;
items[size + 1] = value2;
size += 2;
}
public void add (T value1, T value2, T value3) {
T[] items = this.items;
if (size + 2 >= items.length) items = resize(Math.max(8, (int)(size * 1.75f)));
items[size] = value1;
items[size + 1] = value2;
items[size + 2] = value3;
size += 3;
}
public void add (T value1, T value2, T value3, T value4) {
T[] items = this.items;
if (size + 3 >= items.length) items = resize(Math.max(8, (int)(size * 1.8f))); // 1.75 isn't enough when size=5.
items[size] = value1;
items[size + 1] = value2;
items[size + 2] = value3;
items[size + 3] = value4;
size += 4;
}
public void addAll (Array<? extends T> array) {
addAll(array.items, 0, array.size);
}
public void addAll (Array<? extends T> array, int start, int count) {
if (start + count > array.size)
throw new IllegalArgumentException("start + count must be <= size: " + start + " + " + count + " <= " + array.size);
addAll((T[])array.items, start, count);
}
public void addAll (T... array) {
addAll(array, 0, array.length);
}
public void addAll (T[] array, int start, int count) {
T[] items = this.items;
int sizeNeeded = size + count;
if (sizeNeeded > items.length) items = resize(Math.max(8, (int)(sizeNeeded * 1.75f)));
System.arraycopy(array, start, items, size, count);
size += count;
}
public T get (int index) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
return items[index];
}
public void set (int index, T value) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
items[index] = value;
}
public void insert (int index, T value) {
if (index > size) throw new IndexOutOfBoundsException("index can't be > size: " + index + " > " + size);
T[] items = this.items;
if (size == items.length) items = resize(Math.max(8, (int)(size * 1.75f)));
if (ordered)
System.arraycopy(items, index, items, index + 1, size - index);
else
items[size] = items[index];
size++;
items[index] = value;
}
public void swap (int first, int second) {
if (first >= size) throw new IndexOutOfBoundsException("first can't be >= size: " + first + " >= " + size);
if (second >= size) throw new IndexOutOfBoundsException("second can't be >= size: " + second + " >= " + size);
T[] items = this.items;
T firstValue = items[first];
items[first] = items[second];
items[second] = firstValue;
}
/** Returns if this array contains value.
* @param value May be null.
* @param identity If true, == comparison will be used. If false, .equals() comparison will be used.
* @return true if array contains value, false if it doesn't */
public boolean contains (T value, boolean identity) {
T[] items = this.items;
int i = size - 1;
if (identity || value == null) {
while (i >= 0)
if (items[i--] == value) return true;
} else {
while (i >= 0)
if (value.equals(items[i--])) return true;
}
return false;
}
/** Returns the index of first occurrence of value in the array, or -1 if no such value exists.
* @param value May be null.
* @param identity If true, == comparison will be used. If false, .equals() comparison will be used.
* @return An index of first occurrence of value in array or -1 if no such value exists */
public int indexOf (T value, boolean identity) {
T[] items = this.items;
if (identity || value == null) {
for (int i = 0, n = size; i < n; i++)
if (items[i] == value) return i;
} else {
for (int i = 0, n = size; i < n; i++)
if (value.equals(items[i])) return i;
}
return -1;
}
/** Returns an index of last occurrence of value in array or -1 if no such value exists. Search is started from the end of an
* array.
* @param value May be null.
* @param identity If true, == comparison will be used. If false, .equals() comparison will be used.
* @return An index of last occurrence of value in array or -1 if no such value exists */
public int lastIndexOf (T value, boolean identity) {
T[] items = this.items;
if (identity || value == null) {
for (int i = size - 1; i >= 0; i--)
if (items[i] == value) return i;
} else {
for (int i = size - 1; i >= 0; i--)
if (value.equals(items[i])) return i;
}
return -1;
}
/** Removes the first instance of the specified value in the array.
* @param value May be null.
* @param identity If true, == comparison will be used. If false, .equals() comparison will be used.
* @return true if value was found and removed, false otherwise */
public boolean removeValue (T value, boolean identity) {
T[] items = this.items;
if (identity || value == null) {
for (int i = 0, n = size; i < n; i++) {
if (items[i] == value) {
removeIndex(i);
return true;
}
}
} else {
for (int i = 0, n = size; i < n; i++) {
if (value.equals(items[i])) {
removeIndex(i);
return true;
}
}
}
return false;
}
/** Removes and returns the item at the specified index. */
public T removeIndex (int index) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
T[] items = this.items;
T value = items[index];
size--;
if (ordered)
System.arraycopy(items, index + 1, items, index, size - index);
else
items[index] = items[size];
items[size] = null;
return value;
}
/** Removes the items between the specified indices, inclusive. */
public void removeRange (int start, int end) {
if (end >= size) throw new IndexOutOfBoundsException("end can't be >= size: " + end + " >= " + size);
if (start > end) throw new IndexOutOfBoundsException("start can't be > end: " + start + " > " + end);
T[] items = this.items;
int count = end - start + 1;
if (ordered)
System.arraycopy(items, start + count, items, start, size - (start + count));
else {
int lastIndex = this.size - 1;
for (int i = 0; i < count; i++)
items[start + i] = items[lastIndex - i];
}
size -= count;
}
/** Removes from this array all of elements contained in the specified array.
* @param identity True to use ==, false to use .equals().
* @return true if this array was modified. */
public boolean removeAll (Array<? extends T> array, boolean identity) {
int size = this.size;
int startSize = size;
T[] items = this.items;
if (identity) {
for (int i = 0, n = array.size; i < n; i++) {
T item = array.get(i);
for (int ii = 0; ii < size; ii++) {
if (item == items[ii]) {
removeIndex(ii);
size--;
break;
}
}
}
} else {
for (int i = 0, n = array.size; i < n; i++) {
T item = array.get(i);
for (int ii = 0; ii < size; ii++) {
if (item.equals(items[ii])) {
removeIndex(ii);
size--;
break;
}
}
}
}
return size != startSize;
}
/** Removes and returns the last item. */
public T pop () {
if (size == 0) throw new IllegalStateException("Array is empty.");
--size;
T item = items[size];
items[size] = null;
return item;
}
/** Returns the last item. */
public T peek () {
if (size == 0) throw new IllegalStateException("Array is empty.");
return items[size - 1];
}
/** Returns the first item. */
public T first () {
if (size == 0) throw new IllegalStateException("Array is empty.");
return items[0];
}
/** Returns true if the array is empty. */
public boolean isEmpty () {
return size == 0;
}
public void clear () {
T[] items = this.items;
for (int i = 0, n = size; i < n; i++)
items[i] = null;
size = 0;
}
/** Reduces the size of the backing array to the size of the actual items. This is useful to release memory when many items
* have been removed, or if it is known that more items will not be added.
* @return {@link #items} */
public T[] shrink () {
if (items.length != size) resize(size);
return items;
}
/** Increases the size of the backing array to accommodate the specified number of additional items. Useful before adding many
* items to avoid multiple backing array resizes.
* @return {@link #items} */
public T[] ensureCapacity (int additionalCapacity) {
if (additionalCapacity < 0) throw new IllegalArgumentException("additionalCapacity must be >= 0: " + additionalCapacity);
int sizeNeeded = size + additionalCapacity;
if (sizeNeeded > items.length) resize(Math.max(8, sizeNeeded));
return items;
}
/** Sets the array size, leaving any values beyond the current size null.
* @return {@link #items} */
public T[] setSize (int newSize) {
truncate(newSize);
if (newSize > items.length) resize(Math.max(8, newSize));
size = newSize;
return items;
}
/** Creates a new backing array with the specified size containing the current items. */
protected T[] resize (int newSize) {
T[] items = this.items;
T[] newItems = (T[])java.lang.reflect.Array.newInstance(items.getClass().getComponentType(), newSize);
System.arraycopy(items, 0, newItems, 0, Math.min(size, newItems.length));
this.items = newItems;
return newItems;
}
/** Sorts this array. The array elements must implement {@link Comparable}. This method is not thread safe (uses
* {@link Sort#instance()}). */
public void sort () {
Sort.instance().sort(items, 0, size);
}
/** Sorts the array. This method is not thread safe (uses {@link Sort#instance()}). */
public void sort (Comparator<? super T> comparator) {
Sort.instance().sort(items, comparator, 0, size);
}
/** Selects the nth-lowest element from the Array according to Comparator ranking. This might partially sort the Array. The
* array must have a size greater than 0, or a {@link RuntimeException} will be thrown.
* @see Select
* @param comparator used for comparison
* @param kthLowest rank of desired object according to comparison, n is based on ordinal numbers, not array indices. for min
* value use 1, for max value use size of array, using 0 results in runtime exception.
* @return the value of the Nth lowest ranked object. */
public T selectRanked (Comparator<T> comparator, int kthLowest) {
if (kthLowest < 1) {
throw new RuntimeException("nth_lowest must be greater than 0, 1 = first, 2 = second...");
}
return Select.instance().select(items, comparator, kthLowest, size);
}
/** @see Array#selectRanked(java.util.Comparator, int)
* @param comparator used for comparison
* @param kthLowest rank of desired object according to comparison, n is based on ordinal numbers, not array indices. for min
* value use 1, for max value use size of array, using 0 results in runtime exception.
* @return the index of the Nth lowest ranked object. */
public int selectRankedIndex (Comparator<T> comparator, int kthLowest) {
if (kthLowest < 1) {
throw new RuntimeException("nth_lowest must be greater than 0, 1 = first, 2 = second...");
}
return Select.instance().selectIndex(items, comparator, kthLowest, size);
}
public void reverse () {
T[] items = this.items;
for (int i = 0, lastIndex = size - 1, n = size / 2; i < n; i++) {
int ii = lastIndex - i;
T temp = items[i];
items[i] = items[ii];
items[ii] = temp;
}
}
public void shuffle () {
T[] items = this.items;
for (int i = size - 1; i >= 0; i--) {
int ii = MathUtil.random(i);
T temp = items[i];
items[i] = items[ii];
items[ii] = temp;
}
}
/** Returns an iterator for the items in the array. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link ArrayIterator} constructor for nested or multithreaded iteration. */
@Override
public Iterator<T> iterator () {
if (iterable == null) iterable = new ArrayIterable(this);
return iterable.iterator();
}
/** Returns an iterable for the selected items in the array. Remove is supported, but not between hasNext() and next(). Note
* that the same iterable instance is returned each time this method is called. Use the {@link Predicate.PredicateIterable}
* constructor for nested or multithreaded iteration. */
public Iterable<T> select (Predicate<T> predicate) {
if (predicateIterable == null)
predicateIterable = new Predicate.PredicateIterable<T>(this, predicate);
else
predicateIterable.set(this, predicate);
return predicateIterable;
}
/** Reduces the size of the array to the specified size. If the array is already smaller than the specified size, no action is
* taken. */
public void truncate (int newSize) {
if (newSize < 0) throw new IllegalArgumentException("newSize must be >= 0: " + newSize);
if (size <= newSize) return;
for (int i = newSize; i < size; i++)
items[i] = null;
size = newSize;
}
/** Returns a random item from the array, or null if the array is empty. */
public T random () {
if (size == 0) return null;
return items[MathUtil.random(0, size - 1)];
}
/** Returns the items as an array. Note the array is typed, so the {@link #Array(Class)} constructor must have been used.
* Otherwise use {@link #toArray(Class)} to specify the array type. */
public T[] toArray () {
return (T[])toArray(items.getClass().getComponentType());
}
public <V> V[] toArray (Class<V> type) {
V[] result = (V[])java.lang.reflect.Array.newInstance(type, size);
System.arraycopy(items, 0, result, 0, size);
return result;
}
@Override
public int hashCode () {
if (!ordered) return super.hashCode();
Object[] items = this.items;
int h = 1;
for (int i = 0, n = size; i < n; i++) {
h *= 31;
Object item = items[i];
if (item != null) h += item.hashCode();
}
return h;
}
@Override
public boolean equals (Object object) {
if (object == this) return true;
if (!ordered) return false;
if (!(object instanceof Array)) return false;
Array array = (Array)object;
if (!array.ordered) return false;
int n = size;
if (n != array.size) return false;
Object[] items1 = this.items;
Object[] items2 = array.items;
for (int i = 0; i < n; i++) {
Object o1 = items1[i];
Object o2 = items2[i];
if (!(o1 == null ? o2 == null : o1.equals(o2))) return false;
}
return true;
}
@Override
public String toString () {
if (size == 0) return "[]";
T[] items = this.items;
StringBuilder buffer = new StringBuilder(32);
buffer.append('[');
buffer.append(items[0]);
for (int i = 1; i < size; i++) {
buffer.append(", ");
buffer.append(items[i]);
}
buffer.append(']');
return buffer.toString();
}
public String toString (String separator) {
if (size == 0) return "";
T[] items = this.items;
StringBuilder buffer = new StringBuilder(32);
buffer.append(items[0]);
for (int i = 1; i < size; i++) {
buffer.append(separator);
buffer.append(items[i]);
}
return buffer.toString();
}
/** @see #Array(Class) */
static public <T> Array<T> of (Class<T> arrayType) {
return new Array(arrayType);
}
/** @see #Array(boolean, int, Class) */
static public <T> Array<T> of (boolean ordered, int capacity, Class<T> arrayType) {
return new Array(ordered, capacity, arrayType);
}
/** @see #Array(Object[]) */
static public <T> Array<T> with (T... array) {
return new Array(array);
}
@SuppressWarnings("NullableProblems")
static public class ArrayIterator<T> implements Iterator<T>, Iterable<T> {
private final Array<T> array;
private final boolean allowRemove;
int index;
boolean valid = true;
// ArrayIterable<T> iterable;
public ArrayIterator (Array<T> array) {
this(array, true);
}
public ArrayIterator (Array<T> array, boolean allowRemove) {
this.array = array;
this.allowRemove = allowRemove;
}
@Override
public boolean hasNext () {
if (!valid) {
// System.out.println(iterable.lastAcquire);
throw new RuntimeException("#iterator() cannot be used nested.");
}
return index < array.size;
}
@Override
public T next () {
if (index >= array.size) throw new NoSuchElementException(String.valueOf(index));
if (!valid) {
// System.out.println(iterable.lastAcquire);
throw new RuntimeException("#iterator() cannot be used nested.");
}
return array.items[index++];
}
@Override
public void remove () {
if (!allowRemove) throw new RuntimeException("Remove not allowed.");
index--;
array.removeIndex(index);
}
public void reset () {
index = 0;
}
@Override
public Iterator<T> iterator () {
return this;
}
}
@SuppressWarnings({"unchecked", "NullableProblems"})
static public class ArrayIterable<T> implements Iterable<T> {
private final Array<T> array;
private final boolean allowRemove;
private ArrayIterator iterator1, iterator2;
// java.io.StringWriter lastAcquire = new java.io.StringWriter();
public ArrayIterable (Array<T> array) {
this(array, true);
}
public ArrayIterable (Array<T> array, boolean allowRemove) {
this.array = array;
this.allowRemove = allowRemove;
}
@Override
public Iterator<T> iterator () {
// lastAcquire.getBuffer().setLength(0);
// new Throwable().printStackTrace(new java.io.PrintWriter(lastAcquire));
if (iterator1 == null) {
iterator1 = new ArrayIterator(array, allowRemove);
iterator2 = new ArrayIterator(array, allowRemove);
// iterator1.iterable = this;
// iterator2.iterable = this;
}
if (!iterator1.valid) {
iterator1.index = 0;
iterator1.valid = true;
iterator2.valid = false;
return iterator1;
}
iterator2.index = 0;
iterator2.valid = true;
iterator1.valid = false;
return iterator2;
}
}
}

View File

@ -0,0 +1,660 @@
/*******************************************************************************
* Copyright 2011 LibGDX.
* Mario Zechner <badlogicgames@gmail.com>
* Nathan Sweet <nathan.sweet@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package dorkbox.collections;
import java.util.Iterator;
import java.util.NoSuchElementException;
import dorkbox.collections.ObjectMap.Entry;
/** An ordered or unordered map of objects. This implementation uses arrays to store the keys and values, which means
* {@link #getKey(Object, boolean) gets} do a comparison for each key in the map. This is slower than a typical hash map
* implementation, but may be acceptable for small maps and has the benefits that keys and values can be accessed by index, which
* makes iteration fast. Like {@link Array}, if ordered is false, this class avoids a memory copy when removing elements (the last
* element is moved to the removed element's position).
* @author Nathan Sweet */
@SuppressWarnings({"unchecked", "rawtypes", "unused", "SuspiciousSystemArraycopy", "NullableProblems"})
public class ArrayMap<K, V> implements Iterable<Entry<K, V>> {
public K[] keys;
public V[] values;
public int size;
public boolean ordered;
private Entries entries1, entries2;
private Values valuesIter1, valuesIter2;
private Keys keysIter1, keysIter2;
/** Creates an ordered map with a capacity of 16. */
public ArrayMap () {
this(true, 16);
}
/** Creates an ordered map with the specified capacity. */
public ArrayMap (int capacity) {
this(true, capacity);
}
/** @param ordered If false, methods that remove elements may change the order of other elements in the arrays, which avoids a
* memory copy.
* @param capacity Any elements added beyond this will cause the backing arrays to be grown. */
public ArrayMap (boolean ordered, int capacity) {
this.ordered = ordered;
keys = (K[])new Object[capacity];
values = (V[])new Object[capacity];
}
/** Creates a new map with {@link #keys} and {@link #values} of the specified type.
* @param ordered If false, methods that remove elements may change the order of other elements in the arrays, which avoids a
* memory copy.
* @param capacity Any elements added beyond this will cause the backing arrays to be grown. */
public ArrayMap (boolean ordered, int capacity, Class keyArrayType, Class valueArrayType) {
this.ordered = ordered;
keys = (K[])java.lang.reflect.Array.newInstance(keyArrayType, capacity);
values = (V[])java.lang.reflect.Array.newInstance(valueArrayType, capacity);
}
/** Creates an ordered map with {@link #keys} and {@link #values} of the specified type and a capacity of 16. */
public ArrayMap (Class keyArrayType, Class valueArrayType) {
this(false, 16, keyArrayType, valueArrayType);
}
/** Creates a new map containing the elements in the specified map. The new map will have the same type of backing arrays and
* will be ordered if the specified map is ordered. The capacity is set to the number of elements, so any subsequent elements
* added will cause the backing arrays to be grown. */
@SuppressWarnings("CopyConstructorMissesField")
public ArrayMap (ArrayMap array) {
this(array.ordered, array.size, array.keys.getClass().getComponentType(), array.values.getClass().getComponentType());
size = array.size;
System.arraycopy(array.keys, 0, keys, 0, size);
System.arraycopy(array.values, 0, values, 0, size);
}
public int put (K key, V value) {
int index = indexOfKey(key);
if (index == -1) {
if (size == keys.length) resize(Math.max(8, (int)(size * 1.75f)));
index = size++;
}
keys[index] = key;
values[index] = value;
return index;
}
public int put (K key, V value, int index) {
int existingIndex = indexOfKey(key);
if (existingIndex != -1)
removeIndex(existingIndex);
else if (size == keys.length) //
resize(Math.max(8, (int)(size * 1.75f)));
System.arraycopy(keys, index, keys, index + 1, size - index);
System.arraycopy(values, index, values, index + 1, size - index);
keys[index] = key;
values[index] = value;
size++;
return index;
}
public void putAll (ArrayMap<? extends K, ? extends V> map) {
putAll(map, 0, map.size);
}
public void putAll (ArrayMap<? extends K, ? extends V> map, int offset, int length) {
if (offset + length > map.size)
throw new IllegalArgumentException("offset + length must be <= size: " + offset + " + " + length + " <= " + map.size);
int sizeNeeded = size + length - offset;
if (sizeNeeded >= keys.length) resize(Math.max(8, (int)(sizeNeeded * 1.75f)));
System.arraycopy(map.keys, offset, keys, size, length);
System.arraycopy(map.values, offset, values, size, length);
size += length;
}
/** Returns the value for the specified key. Note this does a .equals() comparison of each key in reverse order until the
* specified key is found. */
public V get (K key) {
Object[] keys = this.keys;
int i = size - 1;
if (key == null) {
for (; i >= 0; i--)
if (keys[i] == key) return values[i];
} else {
for (; i >= 0; i--)
if (key.equals(keys[i])) return values[i];
}
return null;
}
/** Returns the key for the specified value. Note this does a comparison of each value in reverse order until the specified
* value is found.
* @param identity If true, == comparison will be used. If false, .equals() comparison will be used. */
public K getKey (V value, boolean identity) {
Object[] values = this.values;
int i = size - 1;
if (identity || value == null) {
for (; i >= 0; i--)
if (values[i] == value) return keys[i];
} else {
for (; i >= 0; i--)
if (value.equals(values[i])) return keys[i];
}
return null;
}
public K getKeyAt (int index) {
if (index >= size) throw new IndexOutOfBoundsException(String.valueOf(index));
return keys[index];
}
public V getValueAt (int index) {
if (index >= size) throw new IndexOutOfBoundsException(String.valueOf(index));
return values[index];
}
public K firstKey () {
if (size == 0) throw new IllegalStateException("Map is empty.");
return keys[0];
}
public V firstValue () {
if (size == 0) throw new IllegalStateException("Map is empty.");
return values[0];
}
public void setKey (int index, K key) {
if (index >= size) throw new IndexOutOfBoundsException(String.valueOf(index));
keys[index] = key;
}
public void setValue (int index, V value) {
if (index >= size) throw new IndexOutOfBoundsException(String.valueOf(index));
values[index] = value;
}
public void insert (int index, K key, V value) {
if (index > size) throw new IndexOutOfBoundsException(String.valueOf(index));
if (size == keys.length) resize(Math.max(8, (int)(size * 1.75f)));
if (ordered) {
System.arraycopy(keys, index, keys, index + 1, size - index);
System.arraycopy(values, index, values, index + 1, size - index);
} else {
keys[size] = keys[index];
values[size] = values[index];
}
size++;
keys[index] = key;
values[index] = value;
}
public boolean containsKey (K key) {
K[] keys = this.keys;
int i = size - 1;
if (key == null) {
while (i >= 0)
if (keys[i--] == key) return true;
} else {
while (i >= 0)
if (key.equals(keys[i--])) return true;
}
return false;
}
/** @param identity If true, == comparison will be used. If false, .equals() comparison will be used. */
public boolean containsValue (V value, boolean identity) {
V[] values = this.values;
int i = size - 1;
if (identity || value == null) {
while (i >= 0)
if (values[i--] == value) return true;
} else {
while (i >= 0)
if (value.equals(values[i--])) return true;
}
return false;
}
public int indexOfKey (K key) {
Object[] keys = this.keys;
if (key == null) {
for (int i = 0, n = size; i < n; i++)
if (keys[i] == key) return i;
} else {
for (int i = 0, n = size; i < n; i++)
if (key.equals(keys[i])) return i;
}
return -1;
}
public int indexOfValue (V value, boolean identity) {
Object[] values = this.values;
if (identity || value == null) {
for (int i = 0, n = size; i < n; i++)
if (values[i] == value) return i;
} else {
for (int i = 0, n = size; i < n; i++)
if (value.equals(values[i])) return i;
}
return -1;
}
public V removeKey (K key) {
Object[] keys = this.keys;
if (key == null) {
for (int i = 0, n = size; i < n; i++) {
if (keys[i] == key) {
V value = values[i];
removeIndex(i);
return value;
}
}
} else {
for (int i = 0, n = size; i < n; i++) {
if (key.equals(keys[i])) {
V value = values[i];
removeIndex(i);
return value;
}
}
}
return null;
}
public boolean removeValue (V value, boolean identity) {
Object[] values = this.values;
if (identity || value == null) {
for (int i = 0, n = size; i < n; i++) {
if (values[i] == value) {
removeIndex(i);
return true;
}
}
} else {
for (int i = 0, n = size; i < n; i++) {
if (value.equals(values[i])) {
removeIndex(i);
return true;
}
}
}
return false;
}
/** Removes and returns the key/values pair at the specified index. */
public void removeIndex (int index) {
if (index >= size) throw new IndexOutOfBoundsException(String.valueOf(index));
Object[] keys = this.keys;
size--;
if (ordered) {
System.arraycopy(keys, index + 1, keys, index, size - index);
System.arraycopy(values, index + 1, values, index, size - index);
} else {
keys[index] = keys[size];
values[index] = values[size];
}
keys[size] = null;
values[size] = null;
}
/** Returns true if the map is empty. */
public boolean isEmpty () {
return size == 0;
}
/** Returns the last key. */
public K peekKey () {
return keys[size - 1];
}
/** Returns the last value. */
public V peekValue () {
return values[size - 1];
}
/** Clears the map and reduces the size of the backing arrays to be the specified capacity if they are larger. */
public void clear (int maximumCapacity) {
if (keys.length <= maximumCapacity) {
clear();
return;
}
size = 0;
resize(maximumCapacity);
}
public void clear () {
K[] keys = this.keys;
V[] values = this.values;
for (int i = 0, n = size; i < n; i++) {
keys[i] = null;
values[i] = null;
}
size = 0;
}
/** Reduces the size of the backing arrays to the size of the actual number of entries. This is useful to release memory when
* many items have been removed, or if it is known that more entries will not be added. */
public void shrink () {
if (keys.length == size) return;
resize(size);
}
/** Increases the size of the backing arrays to accommodate the specified number of additional entries. Useful before adding
* many entries to avoid multiple backing array resizes. */
public void ensureCapacity (int additionalCapacity) {
if (additionalCapacity < 0) throw new IllegalArgumentException("additionalCapacity must be >= 0: " + additionalCapacity);
int sizeNeeded = size + additionalCapacity;
if (sizeNeeded >= keys.length) resize(Math.max(8, sizeNeeded));
}
protected void resize (int newSize) {
K[] newKeys = (K[])java.lang.reflect.Array.newInstance(keys.getClass().getComponentType(), newSize);
System.arraycopy(keys, 0, newKeys, 0, Math.min(size, newKeys.length));
this.keys = newKeys;
V[] newValues = (V[])java.lang.reflect.Array.newInstance(values.getClass().getComponentType(), newSize);
System.arraycopy(values, 0, newValues, 0, Math.min(size, newValues.length));
this.values = newValues;
}
public void reverse () {
for (int i = 0, lastIndex = size - 1, n = size / 2; i < n; i++) {
int ii = lastIndex - i;
K tempKey = keys[i];
keys[i] = keys[ii];
keys[ii] = tempKey;
V tempValue = values[i];
values[i] = values[ii];
values[ii] = tempValue;
}
}
public void shuffle () {
for (int i = size - 1; i >= 0; i--) {
int ii = MathUtil.random(i);
K tempKey = keys[i];
keys[i] = keys[ii];
keys[ii] = tempKey;
V tempValue = values[i];
values[i] = values[ii];
values[ii] = tempValue;
}
}
/** Reduces the size of the arrays to the specified size. If the arrays are already smaller than the specified size, no action
* is taken. */
public void truncate (int newSize) {
if (size <= newSize) return;
for (int i = newSize; i < size; i++) {
keys[i] = null;
values[i] = null;
}
size = newSize;
}
@Override
public int hashCode () {
K[] keys = this.keys;
V[] values = this.values;
int h = 0;
for (int i = 0, n = size; i < n; i++) {
K key = keys[i];
V value = values[i];
if (key != null) h += key.hashCode() * 31;
if (value != null) h += value.hashCode();
}
return h;
}
@Override
public boolean equals (Object obj) {
if (obj == this) return true;
if (!(obj instanceof ArrayMap)) return false;
ArrayMap<K, V> other = (ArrayMap)obj;
if (other.size != size) return false;
K[] keys = this.keys;
V[] values = this.values;
for (int i = 0, n = size; i < n; i++) {
K key = keys[i];
V value = values[i];
if (value == null) {
if (!other.containsKey(key) || other.get(key) != null) return false;
} else {
if (!value.equals(other.get(key))) return false;
}
}
return true;
}
@Override
public String toString () {
if (size == 0) return "{}";
K[] keys = this.keys;
V[] values = this.values;
StringBuilder buffer = new StringBuilder(32);
buffer.append('{');
buffer.append(keys[0]);
buffer.append('=');
buffer.append(values[0]);
for (int i = 1; i < size; i++) {
buffer.append(", ");
buffer.append(keys[i]);
buffer.append('=');
buffer.append(values[i]);
}
buffer.append('}');
return buffer.toString();
}
@Override
public Iterator<Entry<K, V>> iterator () {
return entries();
}
/** Returns an iterator for the entries in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */
public Entries<K, V> entries () {
if (entries1 == null) {
entries1 = new Entries(this);
entries2 = new Entries(this);
}
if (!entries1.valid) {
entries1.index = 0;
entries1.valid = true;
entries2.valid = false;
return entries1;
}
entries2.index = 0;
entries2.valid = true;
entries1.valid = false;
return entries2;
}
/** Returns an iterator for the values in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */
public Values<V> values () {
if (valuesIter1 == null) {
valuesIter1 = new Values(this);
valuesIter2 = new Values(this);
}
if (!valuesIter1.valid) {
valuesIter1.index = 0;
valuesIter1.valid = true;
valuesIter2.valid = false;
return valuesIter1;
}
valuesIter2.index = 0;
valuesIter2.valid = true;
valuesIter1.valid = false;
return valuesIter2;
}
/** Returns an iterator for the keys in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */
public Keys<K> keys () {
if (keysIter1 == null) {
keysIter1 = new Keys(this);
keysIter2 = new Keys(this);
}
if (!keysIter1.valid) {
keysIter1.index = 0;
keysIter1.valid = true;
keysIter2.valid = false;
return keysIter1;
}
keysIter2.index = 0;
keysIter2.valid = true;
keysIter1.valid = false;
return keysIter2;
}
static public class Entries<K, V> implements Iterable<Entry<K, V>>, Iterator<Entry<K, V>> {
private final ArrayMap<K, V> map;
Entry<K, V> entry = new Entry();
int index;
boolean valid = true;
public Entries (ArrayMap<K, V> map) {
this.map = map;
}
@Override
public boolean hasNext () {
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
return index < map.size;
}
@Override
public Iterator<Entry<K, V>> iterator () {
return this;
}
/** Note the same entry instance is returned each time this method is called. */
@Override
public Entry<K, V> next () {
if (index >= map.size) throw new NoSuchElementException(String.valueOf(index));
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
entry.key = map.keys[index];
entry.value = map.values[index++];
return entry;
}
@Override
public void remove () {
index--;
map.removeIndex(index);
}
public void reset () {
index = 0;
}
}
static public class Values<V> implements Iterable<V>, Iterator<V> {
private final ArrayMap<Object, V> map;
int index;
boolean valid = true;
public Values (ArrayMap<Object, V> map) {
this.map = map;
}
@Override
public boolean hasNext () {
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
return index < map.size;
}
@Override
public Iterator<V> iterator () {
return this;
}
@Override
public V next () {
if (index >= map.size) throw new NoSuchElementException(String.valueOf(index));
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
return map.values[index++];
}
@Override
public void remove () {
index--;
map.removeIndex(index);
}
public void reset () {
index = 0;
}
public Array<V> toArray () {
return new Array(true, map.values, index, map.size - index);
}
public Array<V> toArray (Array array) {
array.addAll(map.values, index, map.size - index);
return array;
}
}
static public class Keys<K> implements Iterable<K>, Iterator<K> {
private final ArrayMap<K, Object> map;
int index;
boolean valid = true;
public Keys (ArrayMap<K, Object> map) {
this.map = map;
}
@Override
public boolean hasNext () {
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
return index < map.size;
}
@Override
public Iterator<K> iterator () {
return this;
}
@Override
public K next () {
if (index >= map.size) throw new NoSuchElementException(String.valueOf(index));
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
return map.keys[index++];
}
@Override
public void remove () {
index--;
map.removeIndex(index);
}
public void reset () {
index = 0;
}
public Array<K> toArray () {
return new Array(true, map.keys, index, map.size - index);
}
public Array<K> toArray (Array array) {
array.addAll(map.keys, index, map.size - index);
return array;
}
}
}

View File

@ -0,0 +1,54 @@
/*
* The MIT License
*
* Copyright 2013 Tim Boudreau.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package dorkbox.collections;
/**
* Bias used to decide how to resolve ambiguity, for example in a binary search
* with a list of timestamps - if the requested timestamp lies between two
* actual data snapshots, should we return the next, previous, nearest one, or
* none unless there is an exact match.
*
* @author Tim Boudreau
*/
public enum Bias {
/**
* If a search result falls between two elements, prefer the next element
*/
FORWARD,
/**
* If a search result falls between two elements, prefer the previous element
*/
BACKWARD,
/**
* If a search result falls between two elements, prefer the element with
* the minimum distance
*/
NEAREST,
/**
* If a search result falls between two elements, return no element unless
* there is an exact match
*/
NONE;
}

View File

@ -0,0 +1,190 @@
/*
* The MIT License
*
* Copyright 2013 Tim Boudreau.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package dorkbox.collections;
import java.util.List;
/**
* General-purpose binary search algorithm; you pass in an array or list
* wrapped in an instance of <code>Indexed</code>, and an
* <code>Evaluator</code> which converts the contents of the list into
* numbers used by the binary search algorithm. Note that the data
* (as returned by the Indexed array/list) <b><i>must be in order from
* low to high</i></b>. The indices need not be contiguous (presumably
* they are not or you wouldn't be using this class), but they must be
* sorted. If assertions are enabled, this is enforced; if not, very
* bad things (endless loops, etc.) can happen as a consequence of passing
* unsorted data in.
* <p/>
* This class is not thread-safe and the size and contents of the <code>Indexed</code>
* should not change while a search is being performed.
*
* @author Tim Boudreau
*/
public class BinarySearch<T> {
private final Evaluator<T> eval;
private final Indexed<T> indexed;
/**
* Create a new binary search.
*
* @param eval The thing which converts elements into numbers
* @param indexed A collection, list or array
*/
public BinarySearch(Evaluator<T> eval, Indexed<T> indexed) {
this.eval = eval;
this.indexed = indexed;
assert checkSorted();
}
public BinarySearch(Evaluator<T> eval, List<T> l) {
this (eval, new ListWrap<T>(l));
}
private boolean checkSorted() {
long val = Long.MIN_VALUE;
long sz = this.indexed.size();
for (long i=0; i < sz; i++) {
T t = this.indexed.get(i);
long nue = this.eval.getValue(t);
if (val != Long.MIN_VALUE) {
if (nue < val) {
throw new IllegalArgumentException("Collection is not sorted at " + i + " - " + this.indexed);
}
}
val = nue;
}
return true;
}
public long search(long value, Bias bias) {
return search(0, this.indexed.size()-1, value, bias);
}
public T match(T prototype, Bias bias) {
long value = this.eval.getValue(prototype);
long index = search(value, bias);
return index == -1 ? null : this.indexed.get(index);
}
public T searchFor(long value, Bias bias) {
long index = search(value, bias);
return index == -1 ? null : this.indexed.get(index);
}
private long search(long start, long end, long value, Bias bias) {
long range = end - start;
if (range == 0) {
return start;
}
if (range == 1) {
T ahead = this.indexed.get(end);
T behind = this.indexed.get(start);
long v1 = this.eval.getValue(behind);
long v2 = this.eval.getValue(ahead);
switch (bias) {
case BACKWARD:
return start;
case FORWARD:
return end;
case NEAREST:
if (v1 == value) {
return start;
} else if (v2 == value) {
return end;
} else {
if (Math.abs(v1 - value) < Math.abs(v2 - value)) {
return start;
} else {
return end;
}
}
case NONE:
if (v1 == value) {
return start;
} else if (v2 == value) {
return end;
} else {
return -1;
}
default:
throw new AssertionError(bias);
}
}
long mid = start + range / 2;
long vm = this.eval.getValue(this.indexed.get(mid));
if (value >= vm) {
return search(mid, end, value, bias);
} else {
return search(start, mid, value, bias);
}
}
/**
* Converts an object into a numeric value that is used to
* perform binary search
* @param <T>
*/
public interface Evaluator<T> {
public long getValue(T obj);
}
/**
* Abstraction for list-like things which have a length and indices
* @param <T>
*/
public interface Indexed<T> {
public T get(long index);
public long size();
}
private static final class ListWrap<T> implements Indexed<T> {
private final List<T> l;
ListWrap(List<T> l) {
this.l = l;
}
@Override
public T get(long index) {
return this.l.get((int) index);
}
@Override
public long size() {
return this.l.size();
}
@Override
public String toString() {
return super.toString() + '{' + this.l + '}';
}
}
}

View File

@ -0,0 +1,363 @@
/*******************************************************************************
* Copyright 2011 LibGDX.
* Mario Zechner <badlogicgames@gmail.com>
* Nathan Sweet <nathan.sweet@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package dorkbox.collections;
import java.util.BitSet;
/** A resizable, ordered or unordered boolean array. Avoids the boxing that occurs with ArrayList<Boolean>. It is less memory
* efficient than {@link BitSet}, except for very small sizes. It more CPU efficient than {@link BitSet}, except for very large
* sizes or if BitSet functionality such as and, or, xor, etc are needed. If unordered, this class avoids a memory copy when
* removing elements (the last element is moved to the removed element's position).
* @author Nathan Sweet */
public class BooleanArray {
public boolean[] items;
public int size;
public boolean ordered;
/** Creates an ordered array with a capacity of 16. */
public BooleanArray () {
this(true, 16);
}
/** Creates an ordered array with the specified capacity. */
public BooleanArray (int capacity) {
this(true, capacity);
}
/** @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a
* memory copy.
* @param capacity Any elements added beyond this will cause the backing array to be grown. */
public BooleanArray (boolean ordered, int capacity) {
this.ordered = ordered;
items = new boolean[capacity];
}
/** Creates a new array containing the elements in the specific array. The new array will be ordered if the specific array is
* ordered. The capacity is set to the number of elements, so any subsequent elements added will cause the backing array to be
* grown. */
public BooleanArray (BooleanArray array) {
this.ordered = array.ordered;
size = array.size;
items = new boolean[size];
System.arraycopy(array.items, 0, items, 0, size);
}
/** Creates a new ordered array containing the elements in the specified array. The capacity is set to the number of elements,
* so any subsequent elements added will cause the backing array to be grown. */
public BooleanArray (boolean[] array) {
this(true, array, 0, array.length);
}
/** Creates a new array containing the elements in the specified array. The capacity is set to the number of elements, so any
* subsequent elements added will cause the backing array to be grown.
* @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a
* memory copy. */
public BooleanArray (boolean ordered, boolean[] array, int startIndex, int count) {
this(ordered, count);
size = count;
System.arraycopy(array, startIndex, items, 0, count);
}
public void add (boolean value) {
boolean[] items = this.items;
if (size == items.length) items = resize(Math.max(8, (int)(size * 1.75f)));
items[size++] = value;
}
public void add (boolean value1, boolean value2) {
boolean[] items = this.items;
if (size + 1 >= items.length) items = resize(Math.max(8, (int)(size * 1.75f)));
items[size] = value1;
items[size + 1] = value2;
size += 2;
}
public void add (boolean value1, boolean value2, boolean value3) {
boolean[] items = this.items;
if (size + 2 >= items.length) items = resize(Math.max(8, (int)(size * 1.75f)));
items[size] = value1;
items[size + 1] = value2;
items[size + 2] = value3;
size += 3;
}
public void add (boolean value1, boolean value2, boolean value3, boolean value4) {
boolean[] items = this.items;
if (size + 3 >= items.length) items = resize(Math.max(8, (int)(size * 1.8f))); // 1.75 isn't enough when size=5.
items[size] = value1;
items[size + 1] = value2;
items[size + 2] = value3;
items[size + 3] = value4;
size += 4;
}
public void addAll (BooleanArray array) {
addAll(array.items, 0, array.size);
}
public void addAll (BooleanArray array, int offset, int length) {
if (offset + length > array.size)
throw new IllegalArgumentException("offset + length must be <= size: " + offset + " + " + length + " <= " + array.size);
addAll(array.items, offset, length);
}
public void addAll (boolean... array) {
addAll(array, 0, array.length);
}
public void addAll (boolean[] array, int offset, int length) {
boolean[] items = this.items;
int sizeNeeded = size + length;
if (sizeNeeded > items.length) items = resize(Math.max(8, (int)(sizeNeeded * 1.75f)));
System.arraycopy(array, offset, items, size, length);
size += length;
}
public boolean get (int index) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
return items[index];
}
public void set (int index, boolean value) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
items[index] = value;
}
public void insert (int index, boolean value) {
if (index > size) throw new IndexOutOfBoundsException("index can't be > size: " + index + " > " + size);
boolean[] items = this.items;
if (size == items.length) items = resize(Math.max(8, (int)(size * 1.75f)));
if (ordered)
System.arraycopy(items, index, items, index + 1, size - index);
else
items[size] = items[index];
size++;
items[index] = value;
}
public void swap (int first, int second) {
if (first >= size) throw new IndexOutOfBoundsException("first can't be >= size: " + first + " >= " + size);
if (second >= size) throw new IndexOutOfBoundsException("second can't be >= size: " + second + " >= " + size);
boolean[] items = this.items;
boolean firstValue = items[first];
items[first] = items[second];
items[second] = firstValue;
}
/** Removes and returns the item at the specified index. */
public boolean removeIndex (int index) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
boolean[] items = this.items;
boolean value = items[index];
size--;
if (ordered)
System.arraycopy(items, index + 1, items, index, size - index);
else
items[index] = items[size];
return value;
}
/** Removes the items between the specified indices, inclusive. */
public void removeRange (int start, int end) {
if (end >= size) throw new IndexOutOfBoundsException("end can't be >= size: " + end + " >= " + size);
if (start > end) throw new IndexOutOfBoundsException("start can't be > end: " + start + " > " + end);
boolean[] items = this.items;
int count = end - start + 1;
if (ordered)
System.arraycopy(items, start + count, items, start, size - (start + count));
else {
int lastIndex = this.size - 1;
for (int i = 0; i < count; i++)
items[start + i] = items[lastIndex - i];
}
size -= count;
}
/** Removes from this array all of elements contained in the specified array.
* @return true if this array was modified. */
public boolean removeAll (BooleanArray array) {
int size = this.size;
int startSize = size;
boolean[] items = this.items;
for (int i = 0, n = array.size; i < n; i++) {
boolean item = array.get(i);
for (int ii = 0; ii < size; ii++) {
if (item == items[ii]) {
removeIndex(ii);
size--;
break;
}
}
}
return size != startSize;
}
/** Removes and returns the last item. */
public boolean pop () {
return items[--size];
}
/** Returns the last item. */
public boolean peek () {
return items[size - 1];
}
/** Returns the first item. */
public boolean first () {
if (size == 0) throw new IllegalStateException("Array is empty.");
return items[0];
}
/** Returns true if the array is empty. */
public boolean isEmpty () {
return size == 0;
}
public void clear () {
size = 0;
}
/** Reduces the size of the backing array to the size of the actual items. This is useful to release memory when many items
* have been removed, or if it is known that more items will not be added.
* @return {@link #items} */
public boolean[] shrink () {
if (items.length != size) resize(size);
return items;
}
/** Increases the size of the backing array to accommodate the specified number of additional items. Useful before adding many
* items to avoid multiple backing array resizes.
* @return {@link #items} */
public boolean[] ensureCapacity (int additionalCapacity) {
if (additionalCapacity < 0) throw new IllegalArgumentException("additionalCapacity must be >= 0: " + additionalCapacity);
int sizeNeeded = size + additionalCapacity;
if (sizeNeeded > items.length) resize(Math.max(8, sizeNeeded));
return items;
}
/** Sets the array size, leaving any values beyond the current size undefined.
* @return {@link #items} */
public boolean[] setSize (int newSize) {
if (newSize < 0) throw new IllegalArgumentException("newSize must be >= 0: " + newSize);
if (newSize > items.length) resize(Math.max(8, newSize));
size = newSize;
return items;
}
protected boolean[] resize (int newSize) {
boolean[] newItems = new boolean[newSize];
boolean[] items = this.items;
System.arraycopy(items, 0, newItems, 0, Math.min(size, newItems.length));
this.items = newItems;
return newItems;
}
public void reverse () {
boolean[] items = this.items;
for (int i = 0, lastIndex = size - 1, n = size / 2; i < n; i++) {
int ii = lastIndex - i;
boolean temp = items[i];
items[i] = items[ii];
items[ii] = temp;
}
}
public void shuffle () {
boolean[] items = this.items;
for (int i = size - 1; i >= 0; i--) {
int ii = MathUtil.random(i);
boolean temp = items[i];
items[i] = items[ii];
items[ii] = temp;
}
}
/** Reduces the size of the array to the specified size. If the array is already smaller than the specified size, no action is
* taken. */
public void truncate (int newSize) {
if (size > newSize) size = newSize;
}
/** Returns a random item from the array, or false if the array is empty. */
public boolean random () {
if (size == 0) return false;
return items[MathUtil.random(0, size - 1)];
}
public boolean[] toArray () {
boolean[] array = new boolean[size];
System.arraycopy(items, 0, array, 0, size);
return array;
}
public int hashCode () {
if (!ordered) return super.hashCode();
boolean[] items = this.items;
int h = 1;
for (int i = 0, n = size; i < n; i++)
h = h * 31 + (items[i] ? 1231 : 1237);
return h;
}
public boolean equals (Object object) {
if (object == this) return true;
if (!ordered) return false;
if (!(object instanceof BooleanArray)) return false;
BooleanArray array = (BooleanArray)object;
if (!array.ordered) return false;
int n = size;
if (n != array.size) return false;
boolean[] items1 = this.items;
boolean[] items2 = array.items;
for (int i = 0; i < n; i++)
if (items1[i] != items2[i]) return false;
return true;
}
public String toString () {
if (size == 0) return "[]";
boolean[] items = this.items;
StringBuilder buffer = new StringBuilder(32);
buffer.append('[');
buffer.append(items[0]);
for (int i = 1; i < size; i++) {
buffer.append(", ");
buffer.append(items[i]);
}
buffer.append(']');
return buffer.toString();
}
public String toString (String separator) {
if (size == 0) return "";
boolean[] items = this.items;
StringBuilder buffer = new StringBuilder(32);
buffer.append(items[0]);
for (int i = 1; i < size; i++) {
buffer.append(separator);
buffer.append(items[i]);
}
return buffer.toString();
}
/** @see #BooleanArray(boolean[]) */
static public BooleanArray with (boolean... array) {
return new BooleanArray(array);
}
}

View File

@ -0,0 +1,408 @@
/*******************************************************************************
* Copyright 2011 LibGDX.
* Mario Zechner <badlogicgames@gmail.com>
* Nathan Sweet <nathan.sweet@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package dorkbox.collections;
import java.util.Arrays;
/** A resizable, ordered or unordered byte array. Avoids the boxing that occurs with ArrayList<Byte>. If unordered, this class
* avoids a memory copy when removing elements (the last element is moved to the removed element's position).
* @author Nathan Sweet */
public class ByteArray {
public byte[] items;
public int size;
public boolean ordered;
/** Creates an ordered array with a capacity of 16. */
public ByteArray () {
this(true, 16);
}
/** Creates an ordered array with the specified capacity. */
public ByteArray (int capacity) {
this(true, capacity);
}
/** @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a
* memory copy.
* @param capacity Any elements added beyond this will cause the backing array to be grown. */
public ByteArray (boolean ordered, int capacity) {
this.ordered = ordered;
items = new byte[capacity];
}
/** Creates a new array containing the elements in the specific array. The new array will be ordered if the specific array is
* ordered. The capacity is set to the number of elements, so any subsequent elements added will cause the backing array to be
* grown. */
public ByteArray (ByteArray array) {
this.ordered = array.ordered;
size = array.size;
items = new byte[size];
System.arraycopy(array.items, 0, items, 0, size);
}
/** Creates a new ordered array containing the elements in the specified array. The capacity is set to the number of elements,
* so any subsequent elements added will cause the backing array to be grown. */
public ByteArray (byte[] array) {
this(true, array, 0, array.length);
}
/** Creates a new array containing the elements in the specified array. The capacity is set to the number of elements, so any
* subsequent elements added will cause the backing array to be grown.
* @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a
* memory copy. */
public ByteArray (boolean ordered, byte[] array, int startIndex, int count) {
this(ordered, count);
size = count;
System.arraycopy(array, startIndex, items, 0, count);
}
public void add (byte value) {
byte[] items = this.items;
if (size == items.length) items = resize(Math.max(8, (int)(size * 1.75f)));
items[size++] = value;
}
public void add (byte value1, byte value2) {
byte[] items = this.items;
if (size + 1 >= items.length) items = resize(Math.max(8, (int)(size * 1.75f)));
items[size] = value1;
items[size + 1] = value2;
size += 2;
}
public void add (byte value1, byte value2, byte value3) {
byte[] items = this.items;
if (size + 2 >= items.length) items = resize(Math.max(8, (int)(size * 1.75f)));
items[size] = value1;
items[size + 1] = value2;
items[size + 2] = value3;
size += 3;
}
public void add (byte value1, byte value2, byte value3, byte value4) {
byte[] items = this.items;
if (size + 3 >= items.length) items = resize(Math.max(8, (int)(size * 1.8f))); // 1.75 isn't enough when size=5.
items[size] = value1;
items[size + 1] = value2;
items[size + 2] = value3;
items[size + 3] = value4;
size += 4;
}
public void addAll (ByteArray array) {
addAll(array.items, 0, array.size);
}
public void addAll (ByteArray array, int offset, int length) {
if (offset + length > array.size)
throw new IllegalArgumentException("offset + length must be <= size: " + offset + " + " + length + " <= " + array.size);
addAll(array.items, offset, length);
}
public void addAll (byte... array) {
addAll(array, 0, array.length);
}
public void addAll (byte[] array, int offset, int length) {
byte[] items = this.items;
int sizeNeeded = size + length;
if (sizeNeeded > items.length) items = resize(Math.max(8, (int)(sizeNeeded * 1.75f)));
System.arraycopy(array, offset, items, size, length);
size += length;
}
public byte get (int index) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
return items[index];
}
public void set (int index, byte value) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
items[index] = value;
}
public void incr (int index, byte value) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
items[index] += value;
}
public void mul (int index, byte value) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
items[index] *= value;
}
public void insert (int index, byte value) {
if (index > size) throw new IndexOutOfBoundsException("index can't be > size: " + index + " > " + size);
byte[] items = this.items;
if (size == items.length) items = resize(Math.max(8, (int)(size * 1.75f)));
if (ordered)
System.arraycopy(items, index, items, index + 1, size - index);
else
items[size] = items[index];
size++;
items[index] = value;
}
public void swap (int first, int second) {
if (first >= size) throw new IndexOutOfBoundsException("first can't be >= size: " + first + " >= " + size);
if (second >= size) throw new IndexOutOfBoundsException("second can't be >= size: " + second + " >= " + size);
byte[] items = this.items;
byte firstValue = items[first];
items[first] = items[second];
items[second] = firstValue;
}
public boolean contains (byte value) {
int i = size - 1;
byte[] items = this.items;
while (i >= 0)
if (items[i--] == value) return true;
return false;
}
public int indexOf (byte value) {
byte[] items = this.items;
for (int i = 0, n = size; i < n; i++)
if (items[i] == value) return i;
return -1;
}
public int lastIndexOf (byte value) {
byte[] items = this.items;
for (int i = size - 1; i >= 0; i--)
if (items[i] == value) return i;
return -1;
}
public boolean removeValue (byte value) {
byte[] items = this.items;
for (int i = 0, n = size; i < n; i++) {
if (items[i] == value) {
removeIndex(i);
return true;
}
}
return false;
}
/** Removes and returns the item at the specified index. */
public int removeIndex (int index) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
byte[] items = this.items;
int value = items[index];
size--;
if (ordered)
System.arraycopy(items, index + 1, items, index, size - index);
else
items[index] = items[size];
return value;
}
/** Removes the items between the specified indices, inclusive. */
public void removeRange (int start, int end) {
if (end >= size) throw new IndexOutOfBoundsException("end can't be >= size: " + end + " >= " + size);
if (start > end) throw new IndexOutOfBoundsException("start can't be > end: " + start + " > " + end);
byte[] items = this.items;
int count = end - start + 1;
if (ordered)
System.arraycopy(items, start + count, items, start, size - (start + count));
else {
int lastIndex = this.size - 1;
for (int i = 0; i < count; i++)
items[start + i] = items[lastIndex - i];
}
size -= count;
}
/** Removes from this array all of elements contained in the specified array.
* @return true if this array was modified. */
public boolean removeAll (ByteArray array) {
int size = this.size;
int startSize = size;
byte[] items = this.items;
for (int i = 0, n = array.size; i < n; i++) {
int item = array.get(i);
for (int ii = 0; ii < size; ii++) {
if (item == items[ii]) {
removeIndex(ii);
size--;
break;
}
}
}
return size != startSize;
}
/** Removes and returns the last item. */
public byte pop () {
return items[--size];
}
/** Returns the last item. */
public byte peek () {
return items[size - 1];
}
/** Returns the first item. */
public byte first () {
if (size == 0) throw new IllegalStateException("Array is empty.");
return items[0];
}
/** Returns true if the array is empty. */
public boolean isEmpty () {
return size == 0;
}
public void clear () {
size = 0;
}
/** Reduces the size of the backing array to the size of the actual items. This is useful to release memory when many items
* have been removed, or if it is known that more items will not be added.
* @return {@link #items} */
public byte[] shrink () {
if (items.length != size) resize(size);
return items;
}
/** Increases the size of the backing array to accommodate the specified number of additional items. Useful before adding many
* items to avoid multiple backing array resizes.
* @return {@link #items} */
public byte[] ensureCapacity (int additionalCapacity) {
if (additionalCapacity < 0) throw new IllegalArgumentException("additionalCapacity must be >= 0: " + additionalCapacity);
int sizeNeeded = size + additionalCapacity;
if (sizeNeeded > items.length) resize(Math.max(8, sizeNeeded));
return items;
}
/** Sets the array size, leaving any values beyond the current size undefined.
* @return {@link #items} */
public byte[] setSize (int newSize) {
if (newSize < 0) throw new IllegalArgumentException("newSize must be >= 0: " + newSize);
if (newSize > items.length) resize(Math.max(8, newSize));
size = newSize;
return items;
}
protected byte[] resize (int newSize) {
byte[] newItems = new byte[newSize];
byte[] items = this.items;
System.arraycopy(items, 0, newItems, 0, Math.min(size, newItems.length));
this.items = newItems;
return newItems;
}
public void sort () {
Arrays.sort(items, 0, size);
}
public void reverse () {
byte[] items = this.items;
for (int i = 0, lastIndex = size - 1, n = size / 2; i < n; i++) {
int ii = lastIndex - i;
byte temp = items[i];
items[i] = items[ii];
items[ii] = temp;
}
}
public void shuffle () {
byte[] items = this.items;
for (int i = size - 1; i >= 0; i--) {
int ii = MathUtil.random(i);
byte temp = items[i];
items[i] = items[ii];
items[ii] = temp;
}
}
/** Reduces the size of the array to the specified size. If the array is already smaller than the specified size, no action is
* taken. */
public void truncate (int newSize) {
if (size > newSize) size = newSize;
}
/** Returns a random item from the array, or zero if the array is empty. */
public byte random () {
if (size == 0) return 0;
return items[MathUtil.random(0, size - 1)];
}
public byte[] toArray () {
byte[] array = new byte[size];
System.arraycopy(items, 0, array, 0, size);
return array;
}
public int hashCode () {
if (!ordered) return super.hashCode();
byte[] items = this.items;
int h = 1;
for (int i = 0, n = size; i < n; i++)
h = h * 31 + items[i];
return h;
}
public boolean equals (Object object) {
if (object == this) return true;
if (!ordered) return false;
if (!(object instanceof ByteArray)) return false;
ByteArray array = (ByteArray)object;
if (!array.ordered) return false;
int n = size;
if (n != array.size) return false;
byte[] items1 = this.items;
byte[] items2 = array.items;
for (int i = 0; i < n; i++)
if (items1[i] != items2[i]) return false;
return true;
}
public String toString () {
if (size == 0) return "[]";
byte[] items = this.items;
StringBuilder buffer = new StringBuilder(32);
buffer.append('[');
buffer.append(items[0]);
for (int i = 1; i < size; i++) {
buffer.append(", ");
buffer.append(items[i]);
}
buffer.append(']');
return buffer.toString();
}
public String toString (String separator) {
if (size == 0) return "";
byte[] items = this.items;
StringBuilder buffer = new StringBuilder(32);
buffer.append(items[0]);
for (int i = 1; i < size; i++) {
buffer.append(separator);
buffer.append(items[i]);
}
return buffer.toString();
}
/** @see #ByteArray(byte[]) */
static public ByteArray with (byte... array) {
return new ByteArray(array);
}
}

View File

@ -0,0 +1,408 @@
/*******************************************************************************
* Copyright 2011 LibGDX.
* Mario Zechner <badlogicgames@gmail.com>
* Nathan Sweet <nathan.sweet@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package dorkbox.collections;
import java.util.Arrays;
/** A resizable, ordered or unordered char array. Avoids the boxing that occurs with ArrayList<Character>. If unordered, this
* class avoids a memory copy when removing elements (the last element is moved to the removed element's position).
* @author Nathan Sweet */
public class CharArray {
public char[] items;
public int size;
public boolean ordered;
/** Creates an ordered array with a capacity of 16. */
public CharArray () {
this(true, 16);
}
/** Creates an ordered array with the specified capacity. */
public CharArray (int capacity) {
this(true, capacity);
}
/** @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a
* memory copy.
* @param capacity Any elements added beyond this will cause the backing array to be grown. */
public CharArray (boolean ordered, int capacity) {
this.ordered = ordered;
items = new char[capacity];
}
/** Creates a new array containing the elements in the specific array. The new array will be ordered if the specific array is
* ordered. The capacity is set to the number of elements, so any subsequent elements added will cause the backing array to be
* grown. */
public CharArray (CharArray array) {
this.ordered = array.ordered;
size = array.size;
items = new char[size];
System.arraycopy(array.items, 0, items, 0, size);
}
/** Creates a new ordered array containing the elements in the specified array. The capacity is set to the number of elements,
* so any subsequent elements added will cause the backing array to be grown. */
public CharArray (char[] array) {
this(true, array, 0, array.length);
}
/** Creates a new array containing the elements in the specified array. The capacity is set to the number of elements, so any
* subsequent elements added will cause the backing array to be grown.
* @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a
* memory copy. */
public CharArray (boolean ordered, char[] array, int startIndex, int count) {
this(ordered, count);
size = count;
System.arraycopy(array, startIndex, items, 0, count);
}
public void add (char value) {
char[] items = this.items;
if (size == items.length) items = resize(Math.max(8, (int)(size * 1.75f)));
items[size++] = value;
}
public void add (char value1, char value2) {
char[] items = this.items;
if (size + 1 >= items.length) items = resize(Math.max(8, (int)(size * 1.75f)));
items[size] = value1;
items[size + 1] = value2;
size += 2;
}
public void add (char value1, char value2, char value3) {
char[] items = this.items;
if (size + 2 >= items.length) items = resize(Math.max(8, (int)(size * 1.75f)));
items[size] = value1;
items[size + 1] = value2;
items[size + 2] = value3;
size += 3;
}
public void add (char value1, char value2, char value3, char value4) {
char[] items = this.items;
if (size + 3 >= items.length) items = resize(Math.max(8, (int)(size * 1.8f))); // 1.75 isn't enough when size=5.
items[size] = value1;
items[size + 1] = value2;
items[size + 2] = value3;
items[size + 3] = value4;
size += 4;
}
public void addAll (CharArray array) {
addAll(array.items, 0, array.size);
}
public void addAll (CharArray array, int offset, int length) {
if (offset + length > array.size)
throw new IllegalArgumentException("offset + length must be <= size: " + offset + " + " + length + " <= " + array.size);
addAll(array.items, offset, length);
}
public void addAll (char... array) {
addAll(array, 0, array.length);
}
public void addAll (char[] array, int offset, int length) {
char[] items = this.items;
int sizeNeeded = size + length;
if (sizeNeeded > items.length) items = resize(Math.max(8, (int)(sizeNeeded * 1.75f)));
System.arraycopy(array, offset, items, size, length);
size += length;
}
public char get (int index) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
return items[index];
}
public void set (int index, char value) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
items[index] = value;
}
public void incr (int index, char value) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
items[index] += value;
}
public void mul (int index, char value) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
items[index] *= value;
}
public void insert (int index, char value) {
if (index > size) throw new IndexOutOfBoundsException("index can't be > size: " + index + " > " + size);
char[] items = this.items;
if (size == items.length) items = resize(Math.max(8, (int)(size * 1.75f)));
if (ordered)
System.arraycopy(items, index, items, index + 1, size - index);
else
items[size] = items[index];
size++;
items[index] = value;
}
public void swap (int first, int second) {
if (first >= size) throw new IndexOutOfBoundsException("first can't be >= size: " + first + " >= " + size);
if (second >= size) throw new IndexOutOfBoundsException("second can't be >= size: " + second + " >= " + size);
char[] items = this.items;
char firstValue = items[first];
items[first] = items[second];
items[second] = firstValue;
}
public boolean contains (char value) {
int i = size - 1;
char[] items = this.items;
while (i >= 0)
if (items[i--] == value) return true;
return false;
}
public int indexOf (char value) {
char[] items = this.items;
for (int i = 0, n = size; i < n; i++)
if (items[i] == value) return i;
return -1;
}
public int lastIndexOf (char value) {
char[] items = this.items;
for (int i = size - 1; i >= 0; i--)
if (items[i] == value) return i;
return -1;
}
public boolean removeValue (char value) {
char[] items = this.items;
for (int i = 0, n = size; i < n; i++) {
if (items[i] == value) {
removeIndex(i);
return true;
}
}
return false;
}
/** Removes and returns the item at the specified index. */
public char removeIndex (int index) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
char[] items = this.items;
char value = items[index];
size--;
if (ordered)
System.arraycopy(items, index + 1, items, index, size - index);
else
items[index] = items[size];
return value;
}
/** Removes the items between the specified indices, inclusive. */
public void removeRange (int start, int end) {
if (end >= size) throw new IndexOutOfBoundsException("end can't be >= size: " + end + " >= " + size);
if (start > end) throw new IndexOutOfBoundsException("start can't be > end: " + start + " > " + end);
char[] items = this.items;
int count = end - start + 1;
if (ordered)
System.arraycopy(items, start + count, items, start, size - (start + count));
else {
int lastIndex = this.size - 1;
for (int i = 0; i < count; i++)
items[start + i] = items[lastIndex - i];
}
size -= count;
}
/** Removes from this array all of elements contained in the specified array.
* @return true if this array was modified. */
public boolean removeAll (CharArray array) {
int size = this.size;
int startSize = size;
char[] items = this.items;
for (int i = 0, n = array.size; i < n; i++) {
char item = array.get(i);
for (int ii = 0; ii < size; ii++) {
if (item == items[ii]) {
removeIndex(ii);
size--;
break;
}
}
}
return size != startSize;
}
/** Removes and returns the last item. */
public char pop () {
return items[--size];
}
/** Returns the last item. */
public char peek () {
return items[size - 1];
}
/** Returns the first item. */
public char first () {
if (size == 0) throw new IllegalStateException("Array is empty.");
return items[0];
}
/** Returns true if the array is empty. */
public boolean isEmpty () {
return size == 0;
}
public void clear () {
size = 0;
}
/** Reduces the size of the backing array to the size of the actual items. This is useful to release memory when many items
* have been removed, or if it is known that more items will not be added.
* @return {@link #items} */
public char[] shrink () {
if (items.length != size) resize(size);
return items;
}
/** Increases the size of the backing array to accommodate the specified number of additional items. Useful before adding many
* items to avoid multiple backing array resizes.
* @return {@link #items} */
public char[] ensureCapacity (int additionalCapacity) {
if (additionalCapacity < 0) throw new IllegalArgumentException("additionalCapacity must be >= 0: " + additionalCapacity);
int sizeNeeded = size + additionalCapacity;
if (sizeNeeded > items.length) resize(Math.max(8, sizeNeeded));
return items;
}
/** Sets the array size, leaving any values beyond the current size undefined.
* @return {@link #items} */
public char[] setSize (int newSize) {
if (newSize < 0) throw new IllegalArgumentException("newSize must be >= 0: " + newSize);
if (newSize > items.length) resize(Math.max(8, newSize));
size = newSize;
return items;
}
protected char[] resize (int newSize) {
char[] newItems = new char[newSize];
char[] items = this.items;
System.arraycopy(items, 0, newItems, 0, Math.min(size, newItems.length));
this.items = newItems;
return newItems;
}
public void sort () {
Arrays.sort(items, 0, size);
}
public void reverse () {
char[] items = this.items;
for (int i = 0, lastIndex = size - 1, n = size / 2; i < n; i++) {
int ii = lastIndex - i;
char temp = items[i];
items[i] = items[ii];
items[ii] = temp;
}
}
public void shuffle () {
char[] items = this.items;
for (int i = size - 1; i >= 0; i--) {
int ii = MathUtil.random(i);
char temp = items[i];
items[i] = items[ii];
items[ii] = temp;
}
}
/** Reduces the size of the array to the specified size. If the array is already smaller than the specified size, no action is
* taken. */
public void truncate (int newSize) {
if (size > newSize) size = newSize;
}
/** Returns a random item from the array, or zero if the array is empty. */
public char random () {
if (size == 0) return 0;
return items[MathUtil.random(0, size - 1)];
}
public char[] toArray () {
char[] array = new char[size];
System.arraycopy(items, 0, array, 0, size);
return array;
}
public int hashCode () {
if (!ordered) return super.hashCode();
char[] items = this.items;
int h = 1;
for (int i = 0, n = size; i < n; i++)
h = h * 31 + items[i];
return h;
}
public boolean equals (Object object) {
if (object == this) return true;
if (!ordered) return false;
if (!(object instanceof CharArray)) return false;
CharArray array = (CharArray)object;
if (!array.ordered) return false;
int n = size;
if (n != array.size) return false;
char[] items1 = this.items;
char[] items2 = array.items;
for (int i = 0; i < n; i++)
if (items1[i] != items2[i]) return false;
return true;
}
public String toString () {
if (size == 0) return "[]";
char[] items = this.items;
StringBuilder buffer = new StringBuilder(32);
buffer.append('[');
buffer.append(items[0]);
for (int i = 1; i < size; i++) {
buffer.append(", ");
buffer.append(items[i]);
}
buffer.append(']');
return buffer.toString();
}
public String toString (String separator) {
if (size == 0) return "";
char[] items = this.items;
StringBuilder buffer = new StringBuilder(32);
buffer.append(items[0]);
for (int i = 1; i < size; i++) {
buffer.append(separator);
buffer.append(items[i]);
}
return buffer.toString();
}
/** @see #CharArray(char[]) */
static public CharArray with (char... array) {
return new CharArray(array);
}
}

View File

@ -0,0 +1,805 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS"
* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
package dorkbox.collections;
/** This is a near duplicate of {@link TimSort}, modified for use with arrays of objects that implement {@link Comparable}, instead
* of using explicit comparators.
*
* <p>
* If you are using an optimizing VM, you may find that ComparableTimSort offers no performance benefit over TimSort in
* conjunction with a comparator that simply returns {@code ((Comparable)first).compareTo(Second)}. If this is the case, you are
* better off deleting ComparableTimSort to eliminate the code duplication. (See Arrays.java for details.) */
class ComparableTimSort {
/** This is the minimum sized sequence that will be merged. Shorter sequences will be lengthened by calling binarySort. If the
* entire array is less than this length, no merges will be performed.
*
* This constant should be a power of two. It was 64 in Tim Peter's C implementation, but 32 was empirically determined to work
* better in this implementation. In the unlikely event that you set this constant to be a number that's not a power of two,
* you'll need to change the {@link #minRunLength} computation.
*
* If you decrease this constant, you must change the stackLen computation in the TimSort constructor, or you risk an
* ArrayOutOfBounds exception. See listsort.txt for a discussion of the minimum stack length required as a function of the
* length of the array being sorted and the minimum merge sequence length. */
private static final int MIN_MERGE = 32;
/** The array being sorted. */
private Object[] a;
/** When we get into galloping mode, we stay there until both runs win less often than MIN_GALLOP consecutive times. */
private static final int MIN_GALLOP = 7;
/** This controls when we get *into* galloping mode. It is initialized to MIN_GALLOP. The mergeLo and mergeHi methods nudge it
* higher for random data, and lower for highly structured data. */
private int minGallop = MIN_GALLOP;
/** Maximum initial size of tmp array, which is used for merging. The array can grow to accommodate demand.
*
* Unlike Tim's original C version, we do not allocate this much storage when sorting smaller arrays. This change was required
* for performance. */
private static final int INITIAL_TMP_STORAGE_LENGTH = 256;
/** Temp storage for merges. */
private Object[] tmp;
private int tmpCount;
/** A stack of pending runs yet to be merged. Run i starts at address base[i] and extends for len[i] elements. It's always true
* (so long as the indices are in bounds) that:
*
* runBase[i] + runLen[i] == runBase[i + 1]
*
* so we could cut the storage for this, but it's a minor amount, and keeping all the info explicit simplifies the code. */
private int stackSize = 0; // Number of pending runs on stack
private final int[] runBase;
private final int[] runLen;
/** Asserts have been placed in if-statements for performace. To enable them, set this field to true and enable them in VM with
* a command line flag. If you modify this class, please do test the asserts! */
private static final boolean DEBUG = false;
ComparableTimSort () {
tmp = new Object[INITIAL_TMP_STORAGE_LENGTH];
runBase = new int[40];
runLen = new int[40];
}
public void doSort (Object[] a, int lo, int hi) {
stackSize = 0;
rangeCheck(a.length, lo, hi);
int nRemaining = hi - lo;
if (nRemaining < 2) return; // Arrays of size 0 and 1 are always sorted
// If array is small, do a "mini-TimSort" with no merges
if (nRemaining < MIN_MERGE) {
int initRunLen = countRunAndMakeAscending(a, lo, hi);
binarySort(a, lo, hi, lo + initRunLen);
return;
}
this.a = a;
tmpCount = 0;
/** March over the array once, left to right, finding natural runs, extending short natural runs to minRun elements, and
* merging runs to maintain stack invariant. */
int minRun = minRunLength(nRemaining);
do {
// Identify next run
int runLen = countRunAndMakeAscending(a, lo, hi);
// If run is short, extend to min(minRun, nRemaining)
if (runLen < minRun) {
int force = nRemaining <= minRun ? nRemaining : minRun;
binarySort(a, lo, lo + force, lo + runLen);
runLen = force;
}
// Push run onto pending-run stack, and maybe merge
pushRun(lo, runLen);
mergeCollapse();
// Advance to find next run
lo += runLen;
nRemaining -= runLen;
} while (nRemaining != 0);
// Merge all remaining runs to complete sort
if (DEBUG) assert lo == hi;
mergeForceCollapse();
if (DEBUG) assert stackSize == 1;
this.a = null;
Object[] tmp = this.tmp;
for (int i = 0, n = tmpCount; i < n; i++)
tmp[i] = null;
}
/** Creates a TimSort instance to maintain the state of an ongoing sort.
*
* @param a the array to be sorted */
private ComparableTimSort (Object[] a) {
this.a = a;
// Allocate temp storage (which may be increased later if necessary)
int len = a.length;
Object[] newArray = new Object[len < 2 * INITIAL_TMP_STORAGE_LENGTH ? len >>> 1 : INITIAL_TMP_STORAGE_LENGTH];
tmp = newArray;
/*
* Allocate runs-to-be-merged stack (which cannot be expanded). The stack length requirements are described in listsort.txt.
* The C version always uses the same stack length (85), but this was measured to be too expensive when sorting "mid-sized"
* arrays (e.g., 100 elements) in Java. Therefore, we use smaller (but sufficiently large) stack lengths for smaller arrays.
* The "magic numbers" in the computation below must be changed if MIN_MERGE is decreased. See the MIN_MERGE declaration
* above for more information.
*/
int stackLen = (len < 120 ? 5 : len < 1542 ? 10 : len < 119151 ? 19 : 40);
runBase = new int[stackLen];
runLen = new int[stackLen];
}
/*
* The next two methods (which are package private and static) constitute the entire API of this class. Each of these methods
* obeys the contract of the public method with the same signature in java.util.Arrays.
*/
static void sort (Object[] a) {
sort(a, 0, a.length);
}
static void sort (Object[] a, int lo, int hi) {
rangeCheck(a.length, lo, hi);
int nRemaining = hi - lo;
if (nRemaining < 2) return; // Arrays of size 0 and 1 are always sorted
// If array is small, do a "mini-TimSort" with no merges
if (nRemaining < MIN_MERGE) {
int initRunLen = countRunAndMakeAscending(a, lo, hi);
binarySort(a, lo, hi, lo + initRunLen);
return;
}
/** March over the array once, left to right, finding natural runs, extending short natural runs to minRun elements, and
* merging runs to maintain stack invariant. */
ComparableTimSort ts = new ComparableTimSort(a);
int minRun = minRunLength(nRemaining);
do {
// Identify next run
int runLen = countRunAndMakeAscending(a, lo, hi);
// If run is short, extend to min(minRun, nRemaining)
if (runLen < minRun) {
int force = nRemaining <= minRun ? nRemaining : minRun;
binarySort(a, lo, lo + force, lo + runLen);
runLen = force;
}
// Push run onto pending-run stack, and maybe merge
ts.pushRun(lo, runLen);
ts.mergeCollapse();
// Advance to find next run
lo += runLen;
nRemaining -= runLen;
} while (nRemaining != 0);
// Merge all remaining runs to complete sort
if (DEBUG) assert lo == hi;
ts.mergeForceCollapse();
if (DEBUG) assert ts.stackSize == 1;
}
/** Sorts the specified portion of the specified array using a binary insertion sort. This is the best method for sorting small
* numbers of elements. It requires O(n log n) compares, but O(n^2) data movement (worst case).
*
* If the initial part of the specified range is already sorted, this method can take advantage of it: the method assumes that
* the elements from index {@code lo}, inclusive, to {@code start}, exclusive are already sorted.
*
* @param a the array in which a range is to be sorted
* @param lo the index of the first element in the range to be sorted
* @param hi the index after the last element in the range to be sorted
* @param start the index of the first element in the range that is not already known to be sorted (@code lo <= start <= hi} */
@SuppressWarnings("fallthrough")
private static void binarySort (Object[] a, int lo, int hi, int start) {
if (DEBUG) assert lo <= start && start <= hi;
if (start == lo) start++;
for (; start < hi; start++) {
@SuppressWarnings("unchecked")
Comparable<Object> pivot = (Comparable)a[start];
// Set left (and right) to the index where a[start] (pivot) belongs
int left = lo;
int right = start;
if (DEBUG) assert left <= right;
/*
* Invariants: pivot >= all in [lo, left). pivot < all in [right, start).
*/
while (left < right) {
int mid = (left + right) >>> 1;
if (pivot.compareTo(a[mid]) < 0)
right = mid;
else
left = mid + 1;
}
if (DEBUG) assert left == right;
/*
* The invariants still hold: pivot >= all in [lo, left) and pivot < all in [left, start), so pivot belongs at left. Note
* that if there are elements equal to pivot, left points to the first slot after them -- that's why this sort is stable.
* Slide elements over to make room to make room for pivot.
*/
int n = start - left; // The number of elements to move
// Switch is just an optimization for arraycopy in default case
switch (n) {
case 2:
a[left + 2] = a[left + 1];
case 1:
a[left + 1] = a[left];
break;
default:
System.arraycopy(a, left, a, left + 1, n);
}
a[left] = pivot;
}
}
/** Returns the length of the run beginning at the specified position in the specified array and reverses the run if it is
* descending (ensuring that the run will always be ascending when the method returns).
*
* A run is the longest ascending sequence with:
*
* a[lo] <= a[lo + 1] <= a[lo + 2] <= ...
*
* or the longest descending sequence with:
*
* a[lo] > a[lo + 1] > a[lo + 2] > ...
*
* For its intended use in a stable mergesort, the strictness of the definition of "descending" is needed so that the call can
* safely reverse a descending sequence without violating stability.
*
* @param a the array in which a run is to be counted and possibly reversed
* @param lo index of the first element in the run
* @param hi index after the last element that may be contained in the run. It is required that @code{lo < hi}.
* @return the length of the run beginning at the specified position in the specified array */
@SuppressWarnings("unchecked")
private static int countRunAndMakeAscending (Object[] a, int lo, int hi) {
if (DEBUG) assert lo < hi;
int runHi = lo + 1;
if (runHi == hi) return 1;
// Find end of run, and reverse range if descending
if (((Comparable)a[runHi++]).compareTo(a[lo]) < 0) { // Descending
while (runHi < hi && ((Comparable)a[runHi]).compareTo(a[runHi - 1]) < 0)
runHi++;
reverseRange(a, lo, runHi);
} else { // Ascending
while (runHi < hi && ((Comparable)a[runHi]).compareTo(a[runHi - 1]) >= 0)
runHi++;
}
return runHi - lo;
}
/** Reverse the specified range of the specified array.
*
* @param a the array in which a range is to be reversed
* @param lo the index of the first element in the range to be reversed
* @param hi the index after the last element in the range to be reversed */
private static void reverseRange (Object[] a, int lo, int hi) {
hi--;
while (lo < hi) {
Object t = a[lo];
a[lo++] = a[hi];
a[hi--] = t;
}
}
/** Returns the minimum acceptable run length for an array of the specified length. Natural runs shorter than this will be
* extended with {@link #binarySort}.
*
* Roughly speaking, the computation is:
*
* If n < MIN_MERGE, return n (it's too small to bother with fancy stuff). Else if n is an exact power of 2, return
* MIN_MERGE/2. Else return an int k, MIN_MERGE/2 <= k <= MIN_MERGE, such that n/k is close to, but strictly less than, an
* exact power of 2.
*
* For the rationale, see listsort.txt.
*
* @param n the length of the array to be sorted
* @return the length of the minimum run to be merged */
private static int minRunLength (int n) {
if (DEBUG) assert n >= 0;
int r = 0; // Becomes 1 if any 1 bits are shifted off
while (n >= MIN_MERGE) {
r |= (n & 1);
n >>= 1;
}
return n + r;
}
/** Pushes the specified run onto the pending-run stack.
*
* @param runBase index of the first element in the run
* @param runLen the number of elements in the run */
private void pushRun (int runBase, int runLen) {
this.runBase[stackSize] = runBase;
this.runLen[stackSize] = runLen;
stackSize++;
}
/** Examines the stack of runs waiting to be merged and merges adjacent runs until the stack invariants are reestablished:
*
* 1. runLen[i - 3] > runLen[i - 2] + runLen[i - 1] 2. runLen[i - 2] > runLen[i - 1]
*
* This method is called each time a new run is pushed onto the stack, so the invariants are guaranteed to hold for i <
* stackSize upon entry to the method. */
private void mergeCollapse () {
while (stackSize > 1) {
int n = stackSize - 2;
if (n > 0 && runLen[n - 1] <= runLen[n] + runLen[n + 1]) {
if (runLen[n - 1] < runLen[n + 1]) n--;
mergeAt(n);
} else if (runLen[n] <= runLen[n + 1]) {
mergeAt(n);
} else {
break; // Invariant is established
}
}
}
/** Merges all runs on the stack until only one remains. This method is called once, to complete the sort. */
private void mergeForceCollapse () {
while (stackSize > 1) {
int n = stackSize - 2;
if (n > 0 && runLen[n - 1] < runLen[n + 1]) n--;
mergeAt(n);
}
}
/** Merges the two runs at stack indices i and i+1. Run i must be the penultimate or antepenultimate run on the stack. In other
* words, i must be equal to stackSize-2 or stackSize-3.
*
* @param i stack index of the first of the two runs to merge */
@SuppressWarnings("unchecked")
private void mergeAt (int i) {
if (DEBUG) assert stackSize >= 2;
if (DEBUG) assert i >= 0;
if (DEBUG) assert i == stackSize - 2 || i == stackSize - 3;
int base1 = runBase[i];
int len1 = runLen[i];
int base2 = runBase[i + 1];
int len2 = runLen[i + 1];
if (DEBUG) assert len1 > 0 && len2 > 0;
if (DEBUG) assert base1 + len1 == base2;
/*
* Record the length of the combined runs; if i is the 3rd-last run now, also slide over the last run (which isn't involved
* in this merge). The current run (i+1) goes away in any case.
*/
runLen[i] = len1 + len2;
if (i == stackSize - 3) {
runBase[i + 1] = runBase[i + 2];
runLen[i + 1] = runLen[i + 2];
}
stackSize--;
/*
* Find where the first element of run2 goes in run1. Prior elements in run1 can be ignored (because they're already in
* place).
*/
int k = gallopRight((Comparable<Object>)a[base2], a, base1, len1, 0);
if (DEBUG) assert k >= 0;
base1 += k;
len1 -= k;
if (len1 == 0) return;
/*
* Find where the last element of run1 goes in run2. Subsequent elements in run2 can be ignored (because they're already in
* place).
*/
len2 = gallopLeft((Comparable<Object>)a[base1 + len1 - 1], a, base2, len2, len2 - 1);
if (DEBUG) assert len2 >= 0;
if (len2 == 0) return;
// Merge remaining runs, using tmp array with min(len1, len2) elements
if (len1 <= len2)
mergeLo(base1, len1, base2, len2);
else
mergeHi(base1, len1, base2, len2);
}
/** Locates the position at which to insert the specified key into the specified sorted range; if the range contains an element
* equal to key, returns the index of the leftmost equal element.
*
* @param key the key whose insertion point to search for
* @param a the array in which to search
* @param base the index of the first element in the range
* @param len the length of the range; must be > 0
* @param hint the index at which to begin the search, 0 <= hint < n. The closer hint is to the result, the faster this method
* will run.
* @return the int k, 0 <= k <= n such that a[b + k - 1] < key <= a[b + k], pretending that a[b - 1] is minus infinity and a[b
* + n] is infinity. In other words, key belongs at index b + k; or in other words, the first k elements of a should
* precede key, and the last n - k should follow it. */
private static int gallopLeft (Comparable<Object> key, Object[] a, int base, int len, int hint) {
if (DEBUG) assert len > 0 && hint >= 0 && hint < len;
int lastOfs = 0;
int ofs = 1;
if (key.compareTo(a[base + hint]) > 0) {
// Gallop right until a[base+hint+lastOfs] < key <= a[base+hint+ofs]
int maxOfs = len - hint;
while (ofs < maxOfs && key.compareTo(a[base + hint + ofs]) > 0) {
lastOfs = ofs;
ofs = (ofs << 1) + 1;
if (ofs <= 0) // int overflow
ofs = maxOfs;
}
if (ofs > maxOfs) ofs = maxOfs;
// Make offsets relative to base
lastOfs += hint;
ofs += hint;
} else { // key <= a[base + hint]
// Gallop left until a[base+hint-ofs] < key <= a[base+hint-lastOfs]
final int maxOfs = hint + 1;
while (ofs < maxOfs && key.compareTo(a[base + hint - ofs]) <= 0) {
lastOfs = ofs;
ofs = (ofs << 1) + 1;
if (ofs <= 0) // int overflow
ofs = maxOfs;
}
if (ofs > maxOfs) ofs = maxOfs;
// Make offsets relative to base
int tmp = lastOfs;
lastOfs = hint - ofs;
ofs = hint - tmp;
}
if (DEBUG) assert -1 <= lastOfs && lastOfs < ofs && ofs <= len;
/*
* Now a[base+lastOfs] < key <= a[base+ofs], so key belongs somewhere to the right of lastOfs but no farther right than ofs.
* Do a binary search, with invariant a[base + lastOfs - 1] < key <= a[base + ofs].
*/
lastOfs++;
while (lastOfs < ofs) {
int m = lastOfs + ((ofs - lastOfs) >>> 1);
if (key.compareTo(a[base + m]) > 0)
lastOfs = m + 1; // a[base + m] < key
else
ofs = m; // key <= a[base + m]
}
if (DEBUG) assert lastOfs == ofs; // so a[base + ofs - 1] < key <= a[base + ofs]
return ofs;
}
/** Like gallopLeft, except that if the range contains an element equal to key, gallopRight returns the index after the
* rightmost equal element.
*
* @param key the key whose insertion point to search for
* @param a the array in which to search
* @param base the index of the first element in the range
* @param len the length of the range; must be > 0
* @param hint the index at which to begin the search, 0 <= hint < n. The closer hint is to the result, the faster this method
* will run.
* @return the int k, 0 <= k <= n such that a[b + k - 1] <= key < a[b + k] */
private static int gallopRight (Comparable<Object> key, Object[] a, int base, int len, int hint) {
if (DEBUG) assert len > 0 && hint >= 0 && hint < len;
int ofs = 1;
int lastOfs = 0;
if (key.compareTo(a[base + hint]) < 0) {
// Gallop left until a[b+hint - ofs] <= key < a[b+hint - lastOfs]
int maxOfs = hint + 1;
while (ofs < maxOfs && key.compareTo(a[base + hint - ofs]) < 0) {
lastOfs = ofs;
ofs = (ofs << 1) + 1;
if (ofs <= 0) // int overflow
ofs = maxOfs;
}
if (ofs > maxOfs) ofs = maxOfs;
// Make offsets relative to b
int tmp = lastOfs;
lastOfs = hint - ofs;
ofs = hint - tmp;
} else { // a[b + hint] <= key
// Gallop right until a[b+hint + lastOfs] <= key < a[b+hint + ofs]
int maxOfs = len - hint;
while (ofs < maxOfs && key.compareTo(a[base + hint + ofs]) >= 0) {
lastOfs = ofs;
ofs = (ofs << 1) + 1;
if (ofs <= 0) // int overflow
ofs = maxOfs;
}
if (ofs > maxOfs) ofs = maxOfs;
// Make offsets relative to b
lastOfs += hint;
ofs += hint;
}
if (DEBUG) assert -1 <= lastOfs && lastOfs < ofs && ofs <= len;
/*
* Now a[b + lastOfs] <= key < a[b + ofs], so key belongs somewhere to the right of lastOfs but no farther right than ofs.
* Do a binary search, with invariant a[b + lastOfs - 1] <= key < a[b + ofs].
*/
lastOfs++;
while (lastOfs < ofs) {
int m = lastOfs + ((ofs - lastOfs) >>> 1);
if (key.compareTo(a[base + m]) < 0)
ofs = m; // key < a[b + m]
else
lastOfs = m + 1; // a[b + m] <= key
}
if (DEBUG) assert lastOfs == ofs; // so a[b + ofs - 1] <= key < a[b + ofs]
return ofs;
}
/** Merges two adjacent runs in place, in a stable fashion. The first element of the first run must be greater than the first
* element of the second run (a[base1] > a[base2]), and the last element of the first run (a[base1 + len1-1]) must be greater
* than all elements of the second run.
*
* For performance, this method should be called only when len1 <= len2; its twin, mergeHi should be called if len1 >= len2.
* (Either method may be called if len1 == len2.)
*
* @param base1 index of first element in first run to be merged
* @param len1 length of first run to be merged (must be > 0)
* @param base2 index of first element in second run to be merged (must be aBase + aLen)
* @param len2 length of second run to be merged (must be > 0) */
@SuppressWarnings("unchecked")
private void mergeLo (int base1, int len1, int base2, int len2) {
if (DEBUG) assert len1 > 0 && len2 > 0 && base1 + len1 == base2;
// Copy first run into temp array
Object[] a = this.a; // For performance
Object[] tmp = ensureCapacity(len1);
System.arraycopy(a, base1, tmp, 0, len1);
int cursor1 = 0; // Indexes into tmp array
int cursor2 = base2; // Indexes int a
int dest = base1; // Indexes int a
// Move first element of second run and deal with degenerate cases
a[dest++] = a[cursor2++];
if (--len2 == 0) {
System.arraycopy(tmp, cursor1, a, dest, len1);
return;
}
if (len1 == 1) {
System.arraycopy(a, cursor2, a, dest, len2);
a[dest + len2] = tmp[cursor1]; // Last elt of run 1 to end of merge
return;
}
int minGallop = this.minGallop; // Use local variable for performance
outer:
while (true) {
int count1 = 0; // Number of times in a row that first run won
int count2 = 0; // Number of times in a row that second run won
/*
* Do the straightforward thing until (if ever) one run starts winning consistently.
*/
do {
if (DEBUG) assert len1 > 1 && len2 > 0;
if (((Comparable)a[cursor2]).compareTo(tmp[cursor1]) < 0) {
a[dest++] = a[cursor2++];
count2++;
count1 = 0;
if (--len2 == 0) break outer;
} else {
a[dest++] = tmp[cursor1++];
count1++;
count2 = 0;
if (--len1 == 1) break outer;
}
} while ((count1 | count2) < minGallop);
/*
* One run is winning so consistently that galloping may be a huge win. So try that, and continue galloping until (if
* ever) neither run appears to be winning consistently anymore.
*/
do {
if (DEBUG) assert len1 > 1 && len2 > 0;
count1 = gallopRight((Comparable)a[cursor2], tmp, cursor1, len1, 0);
if (count1 != 0) {
System.arraycopy(tmp, cursor1, a, dest, count1);
dest += count1;
cursor1 += count1;
len1 -= count1;
if (len1 <= 1) // len1 == 1 || len1 == 0
break outer;
}
a[dest++] = a[cursor2++];
if (--len2 == 0) break outer;
count2 = gallopLeft((Comparable)tmp[cursor1], a, cursor2, len2, 0);
if (count2 != 0) {
System.arraycopy(a, cursor2, a, dest, count2);
dest += count2;
cursor2 += count2;
len2 -= count2;
if (len2 == 0) break outer;
}
a[dest++] = tmp[cursor1++];
if (--len1 == 1) break outer;
minGallop--;
} while (count1 >= MIN_GALLOP | count2 >= MIN_GALLOP);
if (minGallop < 0) minGallop = 0;
minGallop += 2; // Penalize for leaving gallop mode
} // End of "outer" loop
this.minGallop = minGallop < 1 ? 1 : minGallop; // Write back to field
if (len1 == 1) {
if (DEBUG) assert len2 > 0;
System.arraycopy(a, cursor2, a, dest, len2);
a[dest + len2] = tmp[cursor1]; // Last elt of run 1 to end of merge
} else if (len1 == 0) {
throw new IllegalArgumentException("Comparison method violates its general contract!");
} else {
if (DEBUG) assert len2 == 0;
if (DEBUG) assert len1 > 1;
System.arraycopy(tmp, cursor1, a, dest, len1);
}
}
/** Like mergeLo, except that this method should be called only if len1 >= len2; mergeLo should be called if len1 <= len2.
* (Either method may be called if len1 == len2.)
*
* @param base1 index of first element in first run to be merged
* @param len1 length of first run to be merged (must be > 0)
* @param base2 index of first element in second run to be merged (must be aBase + aLen)
* @param len2 length of second run to be merged (must be > 0) */
@SuppressWarnings("unchecked")
private void mergeHi (int base1, int len1, int base2, int len2) {
if (DEBUG) assert len1 > 0 && len2 > 0 && base1 + len1 == base2;
// Copy second run into temp array
Object[] a = this.a; // For performance
Object[] tmp = ensureCapacity(len2);
System.arraycopy(a, base2, tmp, 0, len2);
int cursor1 = base1 + len1 - 1; // Indexes into a
int cursor2 = len2 - 1; // Indexes into tmp array
int dest = base2 + len2 - 1; // Indexes into a
// Move last element of first run and deal with degenerate cases
a[dest--] = a[cursor1--];
if (--len1 == 0) {
System.arraycopy(tmp, 0, a, dest - (len2 - 1), len2);
return;
}
if (len2 == 1) {
dest -= len1;
cursor1 -= len1;
System.arraycopy(a, cursor1 + 1, a, dest + 1, len1);
a[dest] = tmp[cursor2];
return;
}
int minGallop = this.minGallop; // Use local variable for performance
outer:
while (true) {
int count1 = 0; // Number of times in a row that first run won
int count2 = 0; // Number of times in a row that second run won
/*
* Do the straightforward thing until (if ever) one run appears to win consistently.
*/
do {
if (DEBUG) assert len1 > 0 && len2 > 1;
if (((Comparable)tmp[cursor2]).compareTo(a[cursor1]) < 0) {
a[dest--] = a[cursor1--];
count1++;
count2 = 0;
if (--len1 == 0) break outer;
} else {
a[dest--] = tmp[cursor2--];
count2++;
count1 = 0;
if (--len2 == 1) break outer;
}
} while ((count1 | count2) < minGallop);
/*
* One run is winning so consistently that galloping may be a huge win. So try that, and continue galloping until (if
* ever) neither run appears to be winning consistently anymore.
*/
do {
if (DEBUG) assert len1 > 0 && len2 > 1;
count1 = len1 - gallopRight((Comparable)tmp[cursor2], a, base1, len1, len1 - 1);
if (count1 != 0) {
dest -= count1;
cursor1 -= count1;
len1 -= count1;
System.arraycopy(a, cursor1 + 1, a, dest + 1, count1);
if (len1 == 0) break outer;
}
a[dest--] = tmp[cursor2--];
if (--len2 == 1) break outer;
count2 = len2 - gallopLeft((Comparable)a[cursor1], tmp, 0, len2, len2 - 1);
if (count2 != 0) {
dest -= count2;
cursor2 -= count2;
len2 -= count2;
System.arraycopy(tmp, cursor2 + 1, a, dest + 1, count2);
if (len2 <= 1) break outer; // len2 == 1 || len2 == 0
}
a[dest--] = a[cursor1--];
if (--len1 == 0) break outer;
minGallop--;
} while (count1 >= MIN_GALLOP | count2 >= MIN_GALLOP);
if (minGallop < 0) minGallop = 0;
minGallop += 2; // Penalize for leaving gallop mode
} // End of "outer" loop
this.minGallop = minGallop < 1 ? 1 : minGallop; // Write back to field
if (len2 == 1) {
if (DEBUG) assert len1 > 0;
dest -= len1;
cursor1 -= len1;
System.arraycopy(a, cursor1 + 1, a, dest + 1, len1);
a[dest] = tmp[cursor2]; // Move first elt of run2 to front of merge
} else if (len2 == 0) {
throw new IllegalArgumentException("Comparison method violates its general contract!");
} else {
if (DEBUG) assert len1 == 0;
if (DEBUG) assert len2 > 0;
System.arraycopy(tmp, 0, a, dest - (len2 - 1), len2);
}
}
/** Ensures that the external array tmp has at least the specified number of elements, increasing its size if necessary. The
* size increases exponentially to ensure amortized linear time complexity.
*
* @param minCapacity the minimum required capacity of the tmp array
* @return tmp, whether or not it grew */
private Object[] ensureCapacity (int minCapacity) {
tmpCount = Math.max(tmpCount, minCapacity);
if (tmp.length < minCapacity) {
// Compute smallest power of 2 > minCapacity
int newSize = minCapacity;
newSize |= newSize >> 1;
newSize |= newSize >> 2;
newSize |= newSize >> 4;
newSize |= newSize >> 8;
newSize |= newSize >> 16;
newSize++;
if (newSize < 0) // Not bloody likely!
newSize = minCapacity;
else
newSize = Math.min(newSize, a.length >>> 1);
Object[] newArray = new Object[newSize];
tmp = newArray;
}
return tmp;
}
/** Checks that fromIndex and toIndex are in range, and throws an appropriate exception if they aren't.
*
* @param arrayLen the length of the array
* @param fromIndex the index of the first element of the range
* @param toIndex the index after the last element of the range
* @throws IllegalArgumentException if fromIndex > toIndex
* @throws ArrayIndexOutOfBoundsException if fromIndex < 0 or toIndex > arrayLen */
private static void rangeCheck (int arrayLen, int fromIndex, int toIndex) {
if (fromIndex > toIndex) throw new IllegalArgumentException("fromIndex(" + fromIndex + ") > toIndex(" + toIndex + ")");
if (fromIndex < 0) throw new ArrayIndexOutOfBoundsException(fromIndex);
if (toIndex > arrayLen) throw new ArrayIndexOutOfBoundsException(toIndex);
}
}

View File

@ -0,0 +1,76 @@
/*
* Copyright 2015 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.collections;
//
// not thread-safe!!!
//
/**
* @author bennidi
* @author dorkbox, llc Date: 2/3/16
*/
public
class ConcurrentEntry<T> {
private final T value;
private volatile ConcurrentEntry<T> next;
private volatile ConcurrentEntry<T> prev;
public
ConcurrentEntry(T value, ConcurrentEntry<T> next) {
if (next != null) {
this.next = next;
next.prev = this;
}
this.value = value;
}
public
void remove() {
if (this.prev != null) {
this.prev.next = this.next;
if (this.next != null) {
this.next.prev = this.prev;
}
}
else if (this.next != null) {
this.next.prev = null;
}
// can not nullify references to help GC since running iterators might not see the entire set
// if this element is their current element
//next = null;
//prev = null;
}
public
ConcurrentEntry<T> next() {
return this.next;
}
public
void clear() {
this.next = null;
}
public
T getValue() {
return value;
}
}

View File

@ -0,0 +1,147 @@
/*
* Copyright 2015 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.collections;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
/**
* @author dorkbox, llc
*/
@SuppressWarnings("unchecked")
public
class ConcurrentIterator<T> {
/**
* Specifies the load-factor for the IdentityMap used
*/
private float loadFactor = 0.8F;
private static final AtomicInteger ID_COUNTER = new AtomicInteger();
private final int ID = ID_COUNTER.getAndIncrement();
// This is only touched by a single thread, maintains a map of entries for FAST lookup during remove.
private final IdentityMap<T, ConcurrentEntry> entries = new IdentityMap<T, ConcurrentEntry>(32, loadFactor);
// this is still inside the single-writer, and can use the same techniques as subscription manager (for thread safe publication)
@SuppressWarnings("FieldCanBeLocal")
private volatile ConcurrentEntry<T> head = null; // reference to the first element
// Recommended for best performance while adhering to the "single writer principle". Must be static-final
public static final AtomicReferenceFieldUpdater<ConcurrentIterator, ConcurrentEntry> headREF = AtomicReferenceFieldUpdater.newUpdater(
ConcurrentIterator.class,
ConcurrentEntry.class,
"head");
public
ConcurrentIterator() {
}
public
ConcurrentIterator(float loadFactor) {
this.loadFactor = loadFactor;
}
/**
* single writer principle!
* called from within SYNCHRONIZE
*/
public final
void clear() {
this.entries.clear();
this.head = null;
}
/**
* single writer principle!
* called from within SYNCHRONIZE
*
* @param listener the object that will receive messages during publication
*/
public synchronized
void add(final T listener) {
ConcurrentEntry<T> head = headREF.get(this);
if (!entries.containsKey(listener)) {
head = new ConcurrentEntry<T>(listener, head);
entries.put(listener, head);
headREF.lazySet(this, head);
}
}
/**
* single writer principle!
* called from within SYNCHRONIZE
*
* @param listener the object that will NO LONGER receive messages during publication
*/
public synchronized
boolean remove(final T listener) {
ConcurrentEntry<T> concurrentEntry = entries.get(listener);
if (concurrentEntry != null) {
ConcurrentEntry<T> head = headREF.get(this);
if (concurrentEntry == head) {
// if it was second, now it's first
head = head.next();
//oldHead.clear(); // optimize for GC not possible because of potentially running iterators
}
else {
concurrentEntry.remove();
}
headREF.lazySet(this, head);
this.entries.remove(listener);
return true;
}
return false;
}
/**
* single writer principle!
* called from within SYNCHRONIZE
*/
public synchronized
int size() {
return entries.size;
}
@Override
public final
int hashCode() {
return this.ID;
}
@Override
public final
boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ConcurrentIterator other = (ConcurrentIterator) obj;
return this.ID == other.ID;
}
}

View File

@ -0,0 +1,300 @@
/*
* Copyright 2016 zhanhb.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.collections;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* @param <K>
* @param <V>
*
* @author zhanhb
*/
class ConcurrentWeakIdentityHashMap<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> {
private final ConcurrentMap<Key<K>, V> map;
private final ReferenceQueue<K> queue = new ReferenceQueue<>();
private transient Set<Map.Entry<K, V>> es;
ConcurrentWeakIdentityHashMap(int initialCapacity) {
this.map = new ConcurrentHashMap<>(initialCapacity);
}
@SuppressWarnings("CollectionWithoutInitialCapacity")
ConcurrentWeakIdentityHashMap() {
this.map = new ConcurrentHashMap<>();
}
@Override
public
V get(Object key) {
purgeKeys();
return map.get(new Key<>(key, null));
}
@Override
public
V put(K key, V value) {
purgeKeys();
return map.put(new Key<>(key, queue), value);
}
@Override
public
int size() {
purgeKeys();
return map.size();
}
@SuppressWarnings({"NestedAssignment", "element-type-mismatch"})
private
void purgeKeys() {
Reference<? extends K> reference;
while ((reference = queue.poll()) != null) {
map.remove(reference);
}
}
@Override
@SuppressWarnings("NestedAssignment")
public
Set<Map.Entry<K, V>> entrySet() {
Set<Map.Entry<K, V>> entrySet;
return ((entrySet = this.es) == null) ? es = new EntrySet() : entrySet;
}
@Override
public
V putIfAbsent(K key, V value) {
purgeKeys();
return map.putIfAbsent(new Key<>(key, queue), value);
}
@Override
public
V remove(Object key) {
return map.remove(new Key<>(key, null));
}
@Override
public
boolean remove(Object key, Object value) {
purgeKeys();
return map.remove(new Key<>(key, null), value);
}
@Override
public
boolean replace(K key, V oldValue, V newValue) {
purgeKeys();
return map.replace(new Key<>(key, null), oldValue, newValue);
}
@Override
public
V replace(K key, V value) {
purgeKeys();
return map.replace(new Key<>(key, null), value);
}
@Override
public
boolean containsKey(Object key) {
purgeKeys();
return map.containsKey(new Key<>(key, null));
}
@Override
@SuppressWarnings("empty-statement")
public
void clear() {
while (queue.poll() != null) {
}
map.clear();
}
@Override
public
boolean containsValue(Object value) {
purgeKeys();
return map.containsValue(value);
}
private static
class Key<T> extends WeakReference<T> {
private final int hash;
Key(T t, ReferenceQueue<T> queue) {
super(t, queue);
hash = System.identityHashCode(Objects.requireNonNull(t));
}
@Override
public
boolean equals(Object obj) {
return this == obj || obj instanceof Key && ((Key<?>) obj).get() == get();
}
@Override
public
int hashCode() {
return hash;
}
}
private
class Iter implements Iterator<Map.Entry<K, V>> {
private final Iterator<Map.Entry<Key<K>, V>> it;
private Map.Entry<K, V> nextValue;
Iter(Iterator<Map.Entry<Key<K>, V>> it) {
this.it = it;
}
@Override
public
boolean hasNext() {
if (nextValue != null) {
return true;
}
while (it.hasNext()) {
Map.Entry<Key<K>, V> entry = it.next();
K key = entry.getKey().get();
if (key != null) {
nextValue = new Entry(key, entry.getValue());
return true;
}
else {
it.remove();
}
}
return false;
}
@Override
public
Map.Entry<K, V> next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
Map.Entry<K, V> entry = nextValue;
nextValue = null;
return entry;
}
@Override
public
void remove() {
it.remove();
nextValue = null;
}
}
private
class EntrySet extends AbstractSet<Map.Entry<K, V>> {
@Override
public
Iterator<Map.Entry<K, V>> iterator() {
return new Iter(map.entrySet().iterator());
}
@Override
public
int size() {
return ConcurrentWeakIdentityHashMap.this.size();
}
@Override
public
void clear() {
ConcurrentWeakIdentityHashMap.this.clear();
}
@Override
@SuppressWarnings("element-type-mismatch")
public
boolean contains(Object o) {
if (!(o instanceof Map.Entry)) {
return false;
}
Map.Entry<?, ?> e = (Map.Entry<?, ?>) o;
return ConcurrentWeakIdentityHashMap.this.get(e.getKey()) == e.getValue();
}
@Override
public
boolean remove(Object o) {
if (!(o instanceof Map.Entry)) {
return false;
}
Map.Entry<?, ?> e = (Map.Entry<?, ?>) o;
return ConcurrentWeakIdentityHashMap.this.remove(e.getKey(), e.getValue());
}
}
private
class Entry extends SimpleEntry<K, V> {
private static final long serialVersionUID = 1L;
Entry(K key, V value) {
super(key, value);
}
@Override
public
V setValue(V value) {
ConcurrentWeakIdentityHashMap.this.put(getKey(), value);
return super.setValue(value);
}
@Override
public
boolean equals(Object obj) {
if (obj instanceof Map.Entry) {
Map.Entry<?, ?> e = (Map.Entry<?, ?>) obj;
return getKey() == e.getKey() && getValue() == e.getValue();
}
return false;
}
@Override
public
int hashCode() {
return System.identityHashCode(getKey()) ^ System.identityHashCode(getValue());
}
}
}

View File

@ -0,0 +1,424 @@
/*******************************************************************************
* Copyright 2011 LibGDX.
* Mario Zechner <badlogicgames@gmail.com>
* Nathan Sweet <nathan.sweet@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package dorkbox.collections;
import java.util.Arrays;
import java.util.Random;
/** A resizable, ordered or unordered float array. Avoids the boxing that occurs with ArrayList<Float>. If unordered, this class
* avoids a memory copy when removing elements (the last element is moved to the removed element's position).
* @author Nathan Sweet */
public class FloatArray {
public float[] items;
public int size;
public boolean ordered;
/** Creates an ordered array with a capacity of 16. */
public FloatArray () {
this(true, 16);
}
/** Creates an ordered array with the specified capacity. */
public FloatArray (int capacity) {
this(true, capacity);
}
/** @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a
* memory copy.
* @param capacity Any elements added beyond this will cause the backing array to be grown. */
public FloatArray (boolean ordered, int capacity) {
this.ordered = ordered;
items = new float[capacity];
}
/** Creates a new array containing the elements in the specific array. The new array will be ordered if the specific array is
* ordered. The capacity is set to the number of elements, so any subsequent elements added will cause the backing array to be
* grown. */
public FloatArray (FloatArray array) {
this.ordered = array.ordered;
size = array.size;
items = new float[size];
System.arraycopy(array.items, 0, items, 0, size);
}
/** Creates a new ordered array containing the elements in the specified array. The capacity is set to the number of elements,
* so any subsequent elements added will cause the backing array to be grown. */
public FloatArray (float[] array) {
this(true, array, 0, array.length);
}
/** Creates a new array containing the elements in the specified array. The capacity is set to the number of elements, so any
* subsequent elements added will cause the backing array to be grown.
* @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a
* memory copy. */
public FloatArray (boolean ordered, float[] array, int startIndex, int count) {
this(ordered, count);
size = count;
System.arraycopy(array, startIndex, items, 0, count);
}
public void add (float value) {
float[] items = this.items;
if (size == items.length) items = resize(Math.max(8, (int)(size * 1.75f)));
items[size++] = value;
}
public void add (float value1, float value2) {
float[] items = this.items;
if (size + 1 >= items.length) items = resize(Math.max(8, (int)(size * 1.75f)));
items[size] = value1;
items[size + 1] = value2;
size += 2;
}
public void add (float value1, float value2, float value3) {
float[] items = this.items;
if (size + 2 >= items.length) items = resize(Math.max(8, (int)(size * 1.75f)));
items[size] = value1;
items[size + 1] = value2;
items[size + 2] = value3;
size += 3;
}
public void add (float value1, float value2, float value3, float value4) {
float[] items = this.items;
if (size + 3 >= items.length) items = resize(Math.max(8, (int)(size * 1.8f))); // 1.75 isn't enough when size=5.
items[size] = value1;
items[size + 1] = value2;
items[size + 2] = value3;
items[size + 3] = value4;
size += 4;
}
public void addAll (FloatArray array) {
addAll(array.items, 0, array.size);
}
public void addAll (FloatArray array, int offset, int length) {
if (offset + length > array.size)
throw new IllegalArgumentException("offset + length must be <= size: " + offset + " + " + length + " <= " + array.size);
addAll(array.items, offset, length);
}
public void addAll (float... array) {
addAll(array, 0, array.length);
}
public void addAll (float[] array, int offset, int length) {
float[] items = this.items;
int sizeNeeded = size + length;
if (sizeNeeded > items.length) items = resize(Math.max(8, (int)(sizeNeeded * 1.75f)));
System.arraycopy(array, offset, items, size, length);
size += length;
}
public float get (int index) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
return items[index];
}
public void set (int index, float value) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
items[index] = value;
}
public void incr (int index, float value) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
items[index] += value;
}
public void mul (int index, float value) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
items[index] *= value;
}
public void insert (int index, float value) {
if (index > size) throw new IndexOutOfBoundsException("index can't be > size: " + index + " > " + size);
float[] items = this.items;
if (size == items.length) items = resize(Math.max(8, (int)(size * 1.75f)));
if (ordered)
System.arraycopy(items, index, items, index + 1, size - index);
else
items[size] = items[index];
size++;
items[index] = value;
}
public void swap (int first, int second) {
if (first >= size) throw new IndexOutOfBoundsException("first can't be >= size: " + first + " >= " + size);
if (second >= size) throw new IndexOutOfBoundsException("second can't be >= size: " + second + " >= " + size);
float[] items = this.items;
float firstValue = items[first];
items[first] = items[second];
items[second] = firstValue;
}
public boolean contains (float value) {
int i = size - 1;
float[] items = this.items;
while (i >= 0)
if (items[i--] == value) return true;
return false;
}
public int indexOf (float value) {
float[] items = this.items;
for (int i = 0, n = size; i < n; i++)
if (items[i] == value) return i;
return -1;
}
public int lastIndexOf (char value) {
float[] items = this.items;
for (int i = size - 1; i >= 0; i--)
if (items[i] == value) return i;
return -1;
}
public boolean removeValue (float value) {
float[] items = this.items;
for (int i = 0, n = size; i < n; i++) {
if (items[i] == value) {
removeIndex(i);
return true;
}
}
return false;
}
/** Removes and returns the item at the specified index. */
public float removeIndex (int index) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
float[] items = this.items;
float value = items[index];
size--;
if (ordered)
System.arraycopy(items, index + 1, items, index, size - index);
else
items[index] = items[size];
return value;
}
/** Removes the items between the specified indices, inclusive. */
public void removeRange (int start, int end) {
if (end >= size) throw new IndexOutOfBoundsException("end can't be >= size: " + end + " >= " + size);
if (start > end) throw new IndexOutOfBoundsException("start can't be > end: " + start + " > " + end);
float[] items = this.items;
int count = end - start + 1;
if (ordered)
System.arraycopy(items, start + count, items, start, size - (start + count));
else {
int lastIndex = this.size - 1;
for (int i = 0; i < count; i++)
items[start + i] = items[lastIndex - i];
}
size -= count;
}
/** Removes from this array all of elements contained in the specified array.
* @return true if this array was modified. */
public boolean removeAll (FloatArray array) {
int size = this.size;
int startSize = size;
float[] items = this.items;
for (int i = 0, n = array.size; i < n; i++) {
float item = array.get(i);
for (int ii = 0; ii < size; ii++) {
if (item == items[ii]) {
removeIndex(ii);
size--;
break;
}
}
}
return size != startSize;
}
/** Removes and returns the last item. */
public float pop () {
return items[--size];
}
/** Returns the last item. */
public float peek () {
return items[size - 1];
}
/** Returns the first item. */
public float first () {
if (size == 0) throw new IllegalStateException("Array is empty.");
return items[0];
}
/** Returns true if the array is empty. */
public boolean isEmpty () {
return size == 0;
}
public void clear () {
size = 0;
}
/** Reduces the size of the backing array to the size of the actual items. This is useful to release memory when many items
* have been removed, or if it is known that more items will not be added.
* @return {@link #items} */
public float[] shrink () {
if (items.length != size) resize(size);
return items;
}
/** Increases the size of the backing array to accommodate the specified number of additional items. Useful before adding many
* items to avoid multiple backing array resizes.
* @return {@link #items} */
public float[] ensureCapacity (int additionalCapacity) {
if (additionalCapacity < 0) throw new IllegalArgumentException("additionalCapacity must be >= 0: " + additionalCapacity);
int sizeNeeded = size + additionalCapacity;
if (sizeNeeded > items.length) resize(Math.max(8, sizeNeeded));
return items;
}
/** Sets the array size, leaving any values beyond the current size undefined.
* @return {@link #items} */
public float[] setSize (int newSize) {
if (newSize < 0) throw new IllegalArgumentException("newSize must be >= 0: " + newSize);
if (newSize > items.length) resize(Math.max(8, newSize));
size = newSize;
return items;
}
protected float[] resize (int newSize) {
float[] newItems = new float[newSize];
float[] items = this.items;
System.arraycopy(items, 0, newItems, 0, Math.min(size, newItems.length));
this.items = newItems;
return newItems;
}
public void sort () {
Arrays.sort(items, 0, size);
}
public void reverse () {
float[] items = this.items;
for (int i = 0, lastIndex = size - 1, n = size / 2; i < n; i++) {
int ii = lastIndex - i;
float temp = items[i];
items[i] = items[ii];
items[ii] = temp;
}
}
public void shuffle () {
float[] items = this.items;
for (int i = size - 1; i >= 0; i--) {
int ii = MathUtil.random(i);
float temp = items[i];
items[i] = items[ii];
items[ii] = temp;
}
}
/** Reduces the size of the array to the specified size. If the array is already smaller than the specified size, no action is
* taken. */
public void truncate (int newSize) {
if (size > newSize) size = newSize;
}
/** Returns a random item from the array, or zero if the array is empty. */
public float random () {
if (size == 0) return 0;
return items[MathUtil.random(0, size - 1)];
}
public float[] toArray () {
float[] array = new float[size];
System.arraycopy(items, 0, array, 0, size);
return array;
}
public int hashCode () {
if (!ordered) return super.hashCode();
float[] items = this.items;
int h = 1;
for (int i = 0, n = size; i < n; i++)
h = h * 31 + Float.floatToIntBits(items[i]);
return h;
}
public boolean equals (Object object) {
if (object == this) return true;
if (!ordered) return false;
if (!(object instanceof FloatArray)) return false;
FloatArray array = (FloatArray)object;
if (!array.ordered) return false;
int n = size;
if (n != array.size) return false;
float[] items1 = this.items;
float[] items2 = array.items;
for (int i = 0; i < n; i++)
if (items1[i] != items2[i]) return false;
return true;
}
public boolean equals (Object object, float epsilon) {
if (object == this) return true;
if (!(object instanceof FloatArray)) return false;
FloatArray array = (FloatArray)object;
int n = size;
if (n != array.size) return false;
if (!ordered) return false;
if (!array.ordered) return false;
float[] items1 = this.items;
float[] items2 = array.items;
for (int i = 0; i < n; i++)
if (Math.abs(items1[i] - items2[i]) > epsilon) return false;
return true;
}
public String toString () {
if (size == 0) return "[]";
float[] items = this.items;
StringBuilder buffer = new StringBuilder(32);
buffer.append('[');
buffer.append(items[0]);
for (int i = 1; i < size; i++) {
buffer.append(", ");
buffer.append(items[i]);
}
buffer.append(']');
return buffer.toString();
}
public String toString (String separator) {
if (size == 0) return "";
float[] items = this.items;
StringBuilder buffer = new StringBuilder(32);
buffer.append(items[0]);
for (int i = 1; i < size; i++) {
buffer.append(separator);
buffer.append(items[i]);
}
return buffer.toString();
}
/** @see #FloatArray(float[]) */
static public FloatArray with (float... array) {
return new FloatArray(array);
}
}

View File

@ -0,0 +1,812 @@
/*******************************************************************************
* Copyright 2011 LibGDX.
* Mario Zechner <badlogicgames@gmail.com>
* Nathan Sweet <nathan.sweet@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package dorkbox.collections;
import java.util.Iterator;
import java.util.NoSuchElementException;
/** An unordered map that uses identity comparison for keys. This implementation is a cuckoo hash map using 3 hashes, random
* walking, and a small stash for problematic keys. Null keys are not allowed. Null values are allowed. No allocation is done
* except when growing the table size. <br>
* <br>
* This map performs very fast get, containsKey, and remove (typically O(1), worst case O(log(n))). Put may be a bit slower,
* depending on hash collisions. Load factors greater than 0.91 greatly increase the chances the map will have to rehash to the
* next higher POT size.
* @author Nathan Sweet */
@SuppressWarnings({"unchecked", "rawtypes", "NullableProblems"})
public class IdentityMap<K, V> implements Iterable<IdentityMap.Entry<K, V>> {
private static final int PRIME1 = 0xbe1f14b1;
private static final int PRIME2 = 0xb4b82e39;
private static final int PRIME3 = 0xced1c241;
public int size;
K[] keyTable;
V[] valueTable;
int capacity, stashSize;
private float loadFactor;
private int hashShift, mask, threshold;
private int stashCapacity;
private int pushIterations;
private Entries entries1, entries2;
private Values values1, values2;
private Keys keys1, keys2;
/** Creates a new map with an initial capacity of 51 and a load factor of 0.8. */
public IdentityMap () {
this(51, 0.8f);
}
/** Creates a new map with a load factor of 0.8.
* @param initialCapacity If not a power of two, it is increased to the next nearest power of two. */
public IdentityMap (int initialCapacity) {
this(initialCapacity, 0.8f);
}
/** Creates a new map with the specified initial capacity and load factor. This map will hold initialCapacity items before
* growing the backing table.
* @param initialCapacity If not a power of two, it is increased to the next nearest power of two. */
public IdentityMap (int initialCapacity, float loadFactor) {
if (initialCapacity < 0) throw new IllegalArgumentException("initialCapacity must be >= 0: " + initialCapacity);
initialCapacity = MathUtil.nextPowerOfTwo((int)Math.ceil(initialCapacity / loadFactor));
if (initialCapacity > 1 << 30) throw new IllegalArgumentException("initialCapacity is too large: " + initialCapacity);
capacity = initialCapacity;
if (loadFactor <= 0) throw new IllegalArgumentException("loadFactor must be > 0: " + loadFactor);
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
mask = capacity - 1;
hashShift = 31 - Integer.numberOfTrailingZeros(capacity);
stashCapacity = Math.max(3, (int)Math.ceil(Math.log(capacity)) * 2);
pushIterations = Math.max(Math.min(capacity, 8), (int)Math.sqrt(capacity) / 8);
keyTable = (K[])new Object[capacity + stashCapacity];
valueTable = (V[])new Object[keyTable.length];
}
/** Creates a new map identical to the specified map. */
public IdentityMap (IdentityMap map) {
this((int)Math.floor(map.capacity * map.loadFactor), map.loadFactor);
stashSize = map.stashSize;
System.arraycopy(map.keyTable, 0, keyTable, 0, map.keyTable.length);
System.arraycopy(map.valueTable, 0, valueTable, 0, map.valueTable.length);
size = map.size;
}
public V put (K key, V value) {
if (key == null) throw new IllegalArgumentException("key cannot be null.");
K[] keyTable = this.keyTable;
// Check for existing keys.
int hashCode = System.identityHashCode(key);
int index1 = hashCode & mask;
K key1 = keyTable[index1];
if (key1 == key) {
V oldValue = valueTable[index1];
valueTable[index1] = value;
return oldValue;
}
int index2 = hash2(hashCode);
K key2 = keyTable[index2];
if (key2 == key) {
V oldValue = valueTable[index2];
valueTable[index2] = value;
return oldValue;
}
int index3 = hash3(hashCode);
K key3 = keyTable[index3];
if (key3 == key) {
V oldValue = valueTable[index3];
valueTable[index3] = value;
return oldValue;
}
// Update key in the stash.
for (int i = capacity, n = i + stashSize; i < n; i++) {
if (keyTable[i] == key) {
V oldValue = valueTable[i];
valueTable[i] = value;
return oldValue;
}
}
// Check for empty buckets.
if (key1 == null) {
keyTable[index1] = key;
valueTable[index1] = value;
if (size++ >= threshold) resize(capacity << 1);
return null;
}
if (key2 == null) {
keyTable[index2] = key;
valueTable[index2] = value;
if (size++ >= threshold) resize(capacity << 1);
return null;
}
if (key3 == null) {
keyTable[index3] = key;
valueTable[index3] = value;
if (size++ >= threshold) resize(capacity << 1);
return null;
}
push(key, value, index1, key1, index2, key2, index3, key3);
return null;
}
/** Skips checks for existing keys. */
private void putResize (K key, V value) {
// Check for empty buckets.
int hashCode = System.identityHashCode(key);
int index1 = hashCode & mask;
K key1 = keyTable[index1];
if (key1 == null) {
keyTable[index1] = key;
valueTable[index1] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
int index2 = hash2(hashCode);
K key2 = keyTable[index2];
if (key2 == null) {
keyTable[index2] = key;
valueTable[index2] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
int index3 = hash3(hashCode);
K key3 = keyTable[index3];
if (key3 == null) {
keyTable[index3] = key;
valueTable[index3] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
push(key, value, index1, key1, index2, key2, index3, key3);
}
private void push (K insertKey, V insertValue, int index1, K key1, int index2, K key2, int index3, K key3) {
K[] keyTable = this.keyTable;
V[] valueTable = this.valueTable;
int mask = this.mask;
// Push keys until an empty bucket is found.
K evictedKey;
V evictedValue;
int i = 0, pushIterations = this.pushIterations;
do {
// Replace the key and value for one of the hashes.
switch (MathUtil.random.nextInt(2)) {
case 0:
evictedKey = key1;
evictedValue = valueTable[index1];
keyTable[index1] = insertKey;
valueTable[index1] = insertValue;
break;
case 1:
evictedKey = key2;
evictedValue = valueTable[index2];
keyTable[index2] = insertKey;
valueTable[index2] = insertValue;
break;
default:
evictedKey = key3;
evictedValue = valueTable[index3];
keyTable[index3] = insertKey;
valueTable[index3] = insertValue;
break;
}
// If the evicted key hashes to an empty bucket, put it there and stop.
int hashCode = System.identityHashCode(evictedKey);
index1 = hashCode & mask;
key1 = keyTable[index1];
if (key1 == null) {
keyTable[index1] = evictedKey;
valueTable[index1] = evictedValue;
if (size++ >= threshold) resize(capacity << 1);
return;
}
index2 = hash2(hashCode);
key2 = keyTable[index2];
if (key2 == null) {
keyTable[index2] = evictedKey;
valueTable[index2] = evictedValue;
if (size++ >= threshold) resize(capacity << 1);
return;
}
index3 = hash3(hashCode);
key3 = keyTable[index3];
if (key3 == null) {
keyTable[index3] = evictedKey;
valueTable[index3] = evictedValue;
if (size++ >= threshold) resize(capacity << 1);
return;
}
if (++i == pushIterations) break;
insertKey = evictedKey;
insertValue = evictedValue;
} while (true);
putStash(evictedKey, evictedValue);
}
private void putStash (K key, V value) {
if (stashSize == stashCapacity) {
// Too many pushes occurred and the stash is full, increase the table size.
resize(capacity << 1);
putResize(key, value);
return;
}
// Store key in the stash.
int index = capacity + stashSize;
keyTable[index] = key;
valueTable[index] = value;
stashSize++;
size++;
}
public V get (K key) {
int hashCode = System.identityHashCode(key);
int index = hashCode & mask;
if (key != keyTable[index]) {
index = hash2(hashCode);
if (key != keyTable[index]) {
index = hash3(hashCode);
if (key != keyTable[index]) return getStash(key, null);
}
}
return valueTable[index];
}
public V get (K key, V defaultValue) {
int hashCode = System.identityHashCode(key);
int index = hashCode & mask;
if (key != keyTable[index]) {
index = hash2(hashCode);
if (key != keyTable[index]) {
index = hash3(hashCode);
if (key != keyTable[index]) return getStash(key, defaultValue);
}
}
return valueTable[index];
}
private V getStash (K key, V defaultValue) {
K[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++)
if (keyTable[i] == key) return valueTable[i];
return defaultValue;
}
public V remove (K key) {
int hashCode = System.identityHashCode(key);
int index = hashCode & mask;
if (keyTable[index] == key) {
keyTable[index] = null;
V oldValue = valueTable[index];
valueTable[index] = null;
size--;
return oldValue;
}
index = hash2(hashCode);
if (keyTable[index] == key) {
keyTable[index] = null;
V oldValue = valueTable[index];
valueTable[index] = null;
size--;
return oldValue;
}
index = hash3(hashCode);
if (keyTable[index] == key) {
keyTable[index] = null;
V oldValue = valueTable[index];
valueTable[index] = null;
size--;
return oldValue;
}
return removeStash(key);
}
V removeStash (K key) {
K[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++) {
if (keyTable[i] == key) {
V oldValue = valueTable[i];
removeStashIndex(i);
size--;
return oldValue;
}
}
return null;
}
void removeStashIndex (int index) {
// If the removed location was not last, move the last tuple to the removed location.
stashSize--;
int lastIndex = capacity + stashSize;
if (index < lastIndex) {
keyTable[index] = keyTable[lastIndex];
valueTable[index] = valueTable[lastIndex];
valueTable[lastIndex] = null;
} else
valueTable[index] = null;
}
/** Returns true if the map is empty. */
public boolean isEmpty () {
return size == 0;
}
/** Reduces the size of the backing arrays to be the specified capacity or less. If the capacity is already less, nothing is
* done. If the map contains more items than the specified capacity, the next highest power of two capacity is used instead. */
public void shrink (int maximumCapacity) {
if (maximumCapacity < 0) throw new IllegalArgumentException("maximumCapacity must be >= 0: " + maximumCapacity);
if (size > maximumCapacity) maximumCapacity = size;
if (capacity <= maximumCapacity) return;
maximumCapacity = MathUtil.nextPowerOfTwo(maximumCapacity);
resize(maximumCapacity);
}
/** Clears the map and reduces the size of the backing arrays to be the specified capacity if they are larger. */
public void clear (int maximumCapacity) {
if (capacity <= maximumCapacity) {
clear();
return;
}
size = 0;
resize(maximumCapacity);
}
public void clear () {
if (size == 0) return;
K[] keyTable = this.keyTable;
V[] valueTable = this.valueTable;
for (int i = capacity + stashSize; i-- > 0;) {
keyTable[i] = null;
valueTable[i] = null;
}
size = 0;
stashSize = 0;
}
/** Returns true if the specified value is in the map. Note this traverses the entire map and compares every value, which may be
* an expensive operation.
* @param identity If true, uses == to compare the specified value with values in the map. If false, uses
* {@link #equals(Object)}. */
public boolean containsValue (Object value, boolean identity) {
V[] valueTable = this.valueTable;
if (value == null) {
K[] keyTable = this.keyTable;
for (int i = capacity + stashSize; i-- > 0;)
if (keyTable[i] != null && valueTable[i] == null) return true;
} else if (identity) {
for (int i = capacity + stashSize; i-- > 0;)
if (valueTable[i] == value) return true;
} else {
for (int i = capacity + stashSize; i-- > 0;)
if (value.equals(valueTable[i])) return true;
}
return false;
}
public boolean containsKey (K key) {
int hashCode = System.identityHashCode(key);
int index = hashCode & mask;
if (key != keyTable[index]) {
index = hash2(hashCode);
if (key != keyTable[index]) {
index = hash3(hashCode);
if (key != keyTable[index]) return containsKeyStash(key);
}
}
return true;
}
private boolean containsKeyStash (K key) {
K[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++)
if (keyTable[i] == key) return true;
return false;
}
/** Returns the key for the specified value, or null if it is not in the map. Note this traverses the entire map and compares
* every value, which may be an expensive operation.
* @param identity If true, uses == to compare the specified value with values in the map. If false, uses
* {@link #equals(Object)}. */
public K findKey (Object value, boolean identity) {
V[] valueTable = this.valueTable;
if (value == null) {
K[] keyTable = this.keyTable;
for (int i = capacity + stashSize; i-- > 0;)
if (keyTable[i] != null && valueTable[i] == null) return keyTable[i];
} else if (identity) {
for (int i = capacity + stashSize; i-- > 0;)
if (valueTable[i] == value) return keyTable[i];
} else {
for (int i = capacity + stashSize; i-- > 0;)
if (value.equals(valueTable[i])) return keyTable[i];
}
return null;
}
/** Increases the size of the backing array to accommodate the specified number of additional items. Useful before adding many
* items to avoid multiple backing array resizes. */
public void ensureCapacity (int additionalCapacity) {
if (additionalCapacity < 0) throw new IllegalArgumentException("additionalCapacity must be >= 0: " + additionalCapacity);
int sizeNeeded = size + additionalCapacity;
if (sizeNeeded >= threshold) resize(MathUtil.nextPowerOfTwo((int)Math.ceil(sizeNeeded / loadFactor)));
}
private void resize (int newSize) {
int oldEndIndex = capacity + stashSize;
capacity = newSize;
threshold = (int)(newSize * loadFactor);
mask = newSize - 1;
hashShift = 31 - Integer.numberOfTrailingZeros(newSize);
stashCapacity = Math.max(3, (int)Math.ceil(Math.log(newSize)) * 2);
pushIterations = Math.max(Math.min(newSize, 8), (int)Math.sqrt(newSize) / 8);
K[] oldKeyTable = keyTable;
V[] oldValueTable = valueTable;
keyTable = (K[])new Object[newSize + stashCapacity];
valueTable = (V[])new Object[newSize + stashCapacity];
int oldSize = size;
size = 0;
stashSize = 0;
if (oldSize > 0) {
for (int i = 0; i < oldEndIndex; i++) {
K key = oldKeyTable[i];
if (key != null) putResize(key, oldValueTable[i]);
}
}
}
private int hash2 (int h) {
h *= PRIME2;
return (h ^ h >>> hashShift) & mask;
}
private int hash3 (int h) {
h *= PRIME3;
return (h ^ h >>> hashShift) & mask;
}
@Override
public int hashCode () {
int h = 0;
K[] keyTable = this.keyTable;
V[] valueTable = this.valueTable;
for (int i = 0, n = capacity + stashSize; i < n; i++) {
K key = keyTable[i];
if (key != null) {
h += key.hashCode() * 31;
V value = valueTable[i];
if (value != null) {
h += value.hashCode();
}
}
}
return h;
}
@Override
public boolean equals (Object obj) {
if (obj == this) return true;
if (!(obj instanceof IdentityMap)) return false;
IdentityMap<K, V> other = (IdentityMap) obj;
if (other.size != size) return false;
K[] keyTable = this.keyTable;
V[] valueTable = this.valueTable;
for (int i = 0, n = capacity + stashSize; i < n; i++) {
K key = keyTable[i];
if (key != null) {
V value = valueTable[i];
if (value == null) {
if (!other.containsKey(key) || other.get(key) != null) {
return false;
}
} else {
if (!value.equals(other.get(key))) {
return false;
}
}
}
}
return true;
}
@Override
public String toString () {
if (size == 0) return "[]";
StringBuilder buffer = new StringBuilder(32);
buffer.append('[');
K[] keyTable = this.keyTable;
V[] valueTable = this.valueTable;
int i = keyTable.length;
while (i-- > 0) {
K key = keyTable[i];
if (key == null) continue;
buffer.append(key);
buffer.append('=');
buffer.append(valueTable[i]);
break;
}
while (i-- > 0) {
K key = keyTable[i];
if (key == null) continue;
buffer.append(", ");
buffer.append(key);
buffer.append('=');
buffer.append(valueTable[i]);
}
buffer.append(']');
return buffer.toString();
}
@Override
public Iterator<Entry<K, V>> iterator () {
return entries();
}
/** Returns an iterator for the entries in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */
public Entries<K, V> entries () {
if (entries1 == null) {
entries1 = new Entries(this);
entries2 = new Entries(this);
}
if (!entries1.valid) {
entries1.reset();
entries1.valid = true;
entries2.valid = false;
return entries1;
}
entries2.reset();
entries2.valid = true;
entries1.valid = false;
return entries2;
}
/** Returns an iterator for the values in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */
public Values<V> values () {
if (values1 == null) {
values1 = new Values(this);
values2 = new Values(this);
}
if (!values1.valid) {
values1.reset();
values1.valid = true;
values2.valid = false;
return values1;
}
values2.reset();
values2.valid = true;
values1.valid = false;
return values2;
}
/** Returns an iterator for the keys in the map. Remove is supported. Note that the same iterator instance is returned each time
* this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */
public Keys<K> keys () {
if (keys1 == null) {
keys1 = new Keys(this);
keys2 = new Keys(this);
}
if (!keys1.valid) {
keys1.reset();
keys1.valid = true;
keys2.valid = false;
return keys1;
}
keys2.reset();
keys2.valid = true;
keys1.valid = false;
return keys2;
}
static public class Entry<K, V> {
public K key;
public V value;
@Override
public String toString () {
return key + "=" + value;
}
}
@SuppressWarnings("DuplicatedCode")
static private abstract class MapIterator<K, V, I> implements Iterable<I>, Iterator<I> {
public boolean hasNext;
final IdentityMap<K, V> map;
int nextIndex, currentIndex;
boolean valid = true;
public MapIterator (IdentityMap<K, V> map) {
this.map = map;
reset();
}
public void reset () {
currentIndex = -1;
nextIndex = -1;
findNextIndex();
}
void findNextIndex () {
hasNext = false;
K[] keyTable = map.keyTable;
for (int n = map.capacity + map.stashSize; ++nextIndex < n;) {
if (keyTable[nextIndex] != null) {
hasNext = true;
break;
}
}
}
@Override
public void remove () {
if (currentIndex < 0) throw new IllegalStateException("next must be called before remove.");
if (currentIndex >= map.capacity) {
map.removeStashIndex(currentIndex);
nextIndex = currentIndex - 1;
findNextIndex();
} else {
map.keyTable[currentIndex] = null;
map.valueTable[currentIndex] = null;
}
currentIndex = -1;
map.size--;
}
}
@SuppressWarnings({"NullableProblems", "DuplicatedCode", "unchecked", "FieldMayBeFinal"})
static public class Entries<K, V> extends MapIterator<K, V, Entry<K, V>> {
private Entry<K, V> entry = new Entry();
public Entries (IdentityMap<K, V> map) {
super(map);
}
/** Note the same entry instance is returned each time this method is called. */
@Override
public Entry<K, V> next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
K[] keyTable = map.keyTable;
entry.key = keyTable[nextIndex];
entry.value = map.valueTable[nextIndex];
currentIndex = nextIndex;
findNextIndex();
return entry;
}
@Override
public boolean hasNext () {
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
return hasNext;
}
@Override
public Iterator<Entry<K, V>> iterator () {
return this;
}
}
@SuppressWarnings({"rawtypes", "NullableProblems"})
static public class Values<V> extends MapIterator<Object, V, V> {
public Values (IdentityMap<?, V> map) {
super((IdentityMap<Object, V>)map);
}
@Override
public boolean hasNext () {
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
return hasNext;
}
@Override
public V next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
V value = map.valueTable[nextIndex];
currentIndex = nextIndex;
findNextIndex();
return value;
}
@Override
public Iterator<V> iterator () {
return this;
}
/** Returns a new array containing the remaining values. */
public Array<V> toArray () {
Array array = new Array(true, map.size);
while (hasNext)
array.add(next());
return array;
}
/** Adds the remaining values to the specified array. */
public void toArray (Array<V> array) {
while (hasNext)
array.add(next());
}
}
@SuppressWarnings("unchecked")
static public class Keys<K> extends MapIterator<K, Object, K> {
public Keys (IdentityMap<K, ?> map) {
super((IdentityMap<K, Object>)map);
}
@Override
public boolean hasNext () {
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
return hasNext;
}
@Override
public K next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
K key = map.keyTable[nextIndex];
currentIndex = nextIndex;
findNextIndex();
return key;
}
@Override
public Iterator<K> iterator () {
return this;
}
/** Returns a new array containing the remaining keys. */
public Array<K> toArray () {
Array array = new Array(true, map.size);
while (hasNext)
array.add(next());
return array;
}
}
}

View File

@ -0,0 +1,408 @@
/*******************************************************************************
* Copyright 2011 LibGDX.
* Mario Zechner <badlogicgames@gmail.com>
* Nathan Sweet <nathan.sweet@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package dorkbox.collections;
import java.util.Arrays;
/** A resizable, ordered or unordered int array. Avoids the boxing that occurs with ArrayList<Integer>. If unordered, this class
* avoids a memory copy when removing elements (the last element is moved to the removed element's position).
* @author Nathan Sweet */
public class IntArray {
public int[] items;
public int size;
public boolean ordered;
/** Creates an ordered array with a capacity of 16. */
public IntArray () {
this(true, 16);
}
/** Creates an ordered array with the specified capacity. */
public IntArray (int capacity) {
this(true, capacity);
}
/** @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a
* memory copy.
* @param capacity Any elements added beyond this will cause the backing array to be grown. */
public IntArray (boolean ordered, int capacity) {
this.ordered = ordered;
items = new int[capacity];
}
/** Creates a new array containing the elements in the specific array. The new array will be ordered if the specific array is
* ordered. The capacity is set to the number of elements, so any subsequent elements added will cause the backing array to be
* grown. */
public IntArray (IntArray array) {
this.ordered = array.ordered;
size = array.size;
items = new int[size];
System.arraycopy(array.items, 0, items, 0, size);
}
/** Creates a new ordered array containing the elements in the specified array. The capacity is set to the number of elements,
* so any subsequent elements added will cause the backing array to be grown. */
public IntArray (int[] array) {
this(true, array, 0, array.length);
}
/** Creates a new array containing the elements in the specified array. The capacity is set to the number of elements, so any
* subsequent elements added will cause the backing array to be grown.
* @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a
* memory copy. */
public IntArray (boolean ordered, int[] array, int startIndex, int count) {
this(ordered, count);
size = count;
System.arraycopy(array, startIndex, items, 0, count);
}
public void add (int value) {
int[] items = this.items;
if (size == items.length) items = resize(Math.max(8, (int)(size * 1.75f)));
items[size++] = value;
}
public void add (int value1, int value2) {
int[] items = this.items;
if (size + 1 >= items.length) items = resize(Math.max(8, (int)(size * 1.75f)));
items[size] = value1;
items[size + 1] = value2;
size += 2;
}
public void add (int value1, int value2, int value3) {
int[] items = this.items;
if (size + 2 >= items.length) items = resize(Math.max(8, (int)(size * 1.75f)));
items[size] = value1;
items[size + 1] = value2;
items[size + 2] = value3;
size += 3;
}
public void add (int value1, int value2, int value3, int value4) {
int[] items = this.items;
if (size + 3 >= items.length) items = resize(Math.max(8, (int)(size * 1.8f))); // 1.75 isn't enough when size=5.
items[size] = value1;
items[size + 1] = value2;
items[size + 2] = value3;
items[size + 3] = value4;
size += 4;
}
public void addAll (IntArray array) {
addAll(array.items, 0, array.size);
}
public void addAll (IntArray array, int offset, int length) {
if (offset + length > array.size)
throw new IllegalArgumentException("offset + length must be <= size: " + offset + " + " + length + " <= " + array.size);
addAll(array.items, offset, length);
}
public void addAll (int... array) {
addAll(array, 0, array.length);
}
public void addAll (int[] array, int offset, int length) {
int[] items = this.items;
int sizeNeeded = size + length;
if (sizeNeeded > items.length) items = resize(Math.max(8, (int)(sizeNeeded * 1.75f)));
System.arraycopy(array, offset, items, size, length);
size += length;
}
public int get (int index) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
return items[index];
}
public void set (int index, int value) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
items[index] = value;
}
public void incr (int index, int value) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
items[index] += value;
}
public void mul (int index, int value) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
items[index] *= value;
}
public void insert (int index, int value) {
if (index > size) throw new IndexOutOfBoundsException("index can't be > size: " + index + " > " + size);
int[] items = this.items;
if (size == items.length) items = resize(Math.max(8, (int)(size * 1.75f)));
if (ordered)
System.arraycopy(items, index, items, index + 1, size - index);
else
items[size] = items[index];
size++;
items[index] = value;
}
public void swap (int first, int second) {
if (first >= size) throw new IndexOutOfBoundsException("first can't be >= size: " + first + " >= " + size);
if (second >= size) throw new IndexOutOfBoundsException("second can't be >= size: " + second + " >= " + size);
int[] items = this.items;
int firstValue = items[first];
items[first] = items[second];
items[second] = firstValue;
}
public boolean contains (int value) {
int i = size - 1;
int[] items = this.items;
while (i >= 0)
if (items[i--] == value) return true;
return false;
}
public int indexOf (int value) {
int[] items = this.items;
for (int i = 0, n = size; i < n; i++)
if (items[i] == value) return i;
return -1;
}
public int lastIndexOf (int value) {
int[] items = this.items;
for (int i = size - 1; i >= 0; i--)
if (items[i] == value) return i;
return -1;
}
public boolean removeValue (int value) {
int[] items = this.items;
for (int i = 0, n = size; i < n; i++) {
if (items[i] == value) {
removeIndex(i);
return true;
}
}
return false;
}
/** Removes and returns the item at the specified index. */
public int removeIndex (int index) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
int[] items = this.items;
int value = items[index];
size--;
if (ordered)
System.arraycopy(items, index + 1, items, index, size - index);
else
items[index] = items[size];
return value;
}
/** Removes the items between the specified indices, inclusive. */
public void removeRange (int start, int end) {
if (end >= size) throw new IndexOutOfBoundsException("end can't be >= size: " + end + " >= " + size);
if (start > end) throw new IndexOutOfBoundsException("start can't be > end: " + start + " > " + end);
int[] items = this.items;
int count = end - start + 1;
if (ordered)
System.arraycopy(items, start + count, items, start, size - (start + count));
else {
int lastIndex = this.size - 1;
for (int i = 0; i < count; i++)
items[start + i] = items[lastIndex - i];
}
size -= count;
}
/** Removes from this array all of elements contained in the specified array.
* @return true if this array was modified. */
public boolean removeAll (IntArray array) {
int size = this.size;
int startSize = size;
int[] items = this.items;
for (int i = 0, n = array.size; i < n; i++) {
int item = array.get(i);
for (int ii = 0; ii < size; ii++) {
if (item == items[ii]) {
removeIndex(ii);
size--;
break;
}
}
}
return size != startSize;
}
/** Removes and returns the last item. */
public int pop () {
return items[--size];
}
/** Returns the last item. */
public int peek () {
return items[size - 1];
}
/** Returns the first item. */
public int first () {
if (size == 0) throw new IllegalStateException("Array is empty.");
return items[0];
}
/** Returns true if the array is empty. */
public boolean isEmpty () {
return size == 0;
}
public void clear () {
size = 0;
}
/** Reduces the size of the backing array to the size of the actual items. This is useful to release memory when many items
* have been removed, or if it is known that more items will not be added.
* @return {@link #items} */
public int[] shrink () {
if (items.length != size) resize(size);
return items;
}
/** Increases the size of the backing array to accommodate the specified number of additional items. Useful before adding many
* items to avoid multiple backing array resizes.
* @return {@link #items} */
public int[] ensureCapacity (int additionalCapacity) {
if (additionalCapacity < 0) throw new IllegalArgumentException("additionalCapacity must be >= 0: " + additionalCapacity);
int sizeNeeded = size + additionalCapacity;
if (sizeNeeded > items.length) resize(Math.max(8, sizeNeeded));
return items;
}
/** Sets the array size, leaving any values beyond the current size undefined.
* @return {@link #items} */
public int[] setSize (int newSize) {
if (newSize < 0) throw new IllegalArgumentException("newSize must be >= 0: " + newSize);
if (newSize > items.length) resize(Math.max(8, newSize));
size = newSize;
return items;
}
protected int[] resize (int newSize) {
int[] newItems = new int[newSize];
int[] items = this.items;
System.arraycopy(items, 0, newItems, 0, Math.min(size, newItems.length));
this.items = newItems;
return newItems;
}
public void sort () {
Arrays.sort(items, 0, size);
}
public void reverse () {
int[] items = this.items;
for (int i = 0, lastIndex = size - 1, n = size / 2; i < n; i++) {
int ii = lastIndex - i;
int temp = items[i];
items[i] = items[ii];
items[ii] = temp;
}
}
public void shuffle () {
int[] items = this.items;
for (int i = size - 1; i >= 0; i--) {
int ii = MathUtil.random(i);
int temp = items[i];
items[i] = items[ii];
items[ii] = temp;
}
}
/** Reduces the size of the array to the specified size. If the array is already smaller than the specified size, no action is
* taken. */
public void truncate (int newSize) {
if (size > newSize) size = newSize;
}
/** Returns a random item from the array, or zero if the array is empty. */
public int random () {
if (size == 0) return 0;
return items[MathUtil.random(0, size - 1)];
}
public int[] toArray () {
int[] array = new int[size];
System.arraycopy(items, 0, array, 0, size);
return array;
}
public int hashCode () {
if (!ordered) return super.hashCode();
int[] items = this.items;
int h = 1;
for (int i = 0, n = size; i < n; i++)
h = h * 31 + items[i];
return h;
}
public boolean equals (Object object) {
if (object == this) return true;
if (!ordered) return false;
if (!(object instanceof IntArray)) return false;
IntArray array = (IntArray)object;
if (!array.ordered) return false;
int n = size;
if (n != array.size) return false;
int[] items1 = this.items;
int[] items2 = array.items;
for (int i = 0; i < n; i++)
if (items[i] != array.items[i]) return false;
return true;
}
public String toString () {
if (size == 0) return "[]";
int[] items = this.items;
StringBuilder buffer = new StringBuilder(32);
buffer.append('[');
buffer.append(items[0]);
for (int i = 1; i < size; i++) {
buffer.append(", ");
buffer.append(items[i]);
}
buffer.append(']');
return buffer.toString();
}
public String toString (String separator) {
if (size == 0) return "";
int[] items = this.items;
StringBuilder buffer = new StringBuilder(32);
buffer.append(items[0]);
for (int i = 1; i < size; i++) {
buffer.append(separator);
buffer.append(items[i]);
}
return buffer.toString();
}
/** @see #IntArray(int[]) */
static public IntArray with (int... array) {
return new IntArray(array);
}
}

View File

@ -0,0 +1,842 @@
/*******************************************************************************
* Copyright 2011 LibGDX.
* Mario Zechner <badlogicgames@gmail.com>
* Nathan Sweet <nathan.sweet@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package dorkbox.collections;
import java.util.Iterator;
import java.util.NoSuchElementException;
/** An unordered map where the keys are ints and values are floats. This implementation is a cuckoo hash map using 3 hashes, random
* walking, and a small stash for problematic keys. Null keys are not allowed. No allocation is done except when growing the table
* size. <br>
* <br>
* This map performs very fast get, containsKey, and remove (typically O(1), worst case O(log(n))). Put may be a bit slower,
* depending on hash collisions. Load factors greater than 0.91 greatly increase the chances the map will have to rehash to the
* next higher POT size.
* @author Nathan Sweet */
public class IntFloatMap implements Iterable<IntFloatMap.Entry> {
private static final int PRIME1 = 0xbe1f14b1;
private static final int PRIME2 = 0xb4b82e39;
private static final int PRIME3 = 0xced1c241;
private static final int EMPTY = 0;
public int size;
int[] keyTable;
float[] valueTable;
int capacity, stashSize;
float zeroValue;
boolean hasZeroValue;
private float loadFactor;
private int hashShift, mask, threshold;
private int stashCapacity;
private int pushIterations;
private Entries entries1, entries2;
private Values values1, values2;
private Keys keys1, keys2;
/** Creates a new map with an initial capacity of 51 and a load factor of 0.8. */
public IntFloatMap () {
this(51, 0.8f);
}
/** Creates a new map with a load factor of 0.8.
* @param initialCapacity If not a power of two, it is increased to the next nearest power of two. */
public IntFloatMap (int initialCapacity) {
this(initialCapacity, 0.8f);
}
/** Creates a new map with the specified initial capacity and load factor. This map will hold initialCapacity items before
* growing the backing table.
* @param initialCapacity If not a power of two, it is increased to the next nearest power of two. */
public IntFloatMap (int initialCapacity, float loadFactor) {
if (initialCapacity < 0) throw new IllegalArgumentException("initialCapacity must be >= 0: " + initialCapacity);
initialCapacity = MathUtil.nextPowerOfTwo((int)Math.ceil(initialCapacity / loadFactor));
if (initialCapacity > 1 << 30) throw new IllegalArgumentException("initialCapacity is too large: " + initialCapacity);
capacity = initialCapacity;
if (loadFactor <= 0) throw new IllegalArgumentException("loadFactor must be > 0: " + loadFactor);
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
mask = capacity - 1;
hashShift = 31 - Integer.numberOfTrailingZeros(capacity);
stashCapacity = Math.max(3, (int)Math.ceil(Math.log(capacity)) * 2);
pushIterations = Math.max(Math.min(capacity, 8), (int)Math.sqrt(capacity) / 8);
keyTable = new int[capacity + stashCapacity];
valueTable = new float[keyTable.length];
}
/** Creates a new map identical to the specified map. */
public IntFloatMap (IntFloatMap map) {
this((int)Math.floor(map.capacity * map.loadFactor), map.loadFactor);
stashSize = map.stashSize;
System.arraycopy(map.keyTable, 0, keyTable, 0, map.keyTable.length);
System.arraycopy(map.valueTable, 0, valueTable, 0, map.valueTable.length);
size = map.size;
zeroValue = map.zeroValue;
hasZeroValue = map.hasZeroValue;
}
public void put (int key, float value) {
if (key == 0) {
zeroValue = value;
if (!hasZeroValue) {
hasZeroValue = true;
size++;
}
return;
}
int[] keyTable = this.keyTable;
// Check for existing keys.
int index1 = key & mask;
int key1 = keyTable[index1];
if (key == key1) {
valueTable[index1] = value;
return;
}
int index2 = hash2(key);
int key2 = keyTable[index2];
if (key == key2) {
valueTable[index2] = value;
return;
}
int index3 = hash3(key);
int key3 = keyTable[index3];
if (key == key3) {
valueTable[index3] = value;
return;
}
// Update key in the stash.
for (int i = capacity, n = i + stashSize; i < n; i++) {
if (key == keyTable[i]) {
valueTable[i] = value;
return;
}
}
// Check for empty buckets.
if (key1 == EMPTY) {
keyTable[index1] = key;
valueTable[index1] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
if (key2 == EMPTY) {
keyTable[index2] = key;
valueTable[index2] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
if (key3 == EMPTY) {
keyTable[index3] = key;
valueTable[index3] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
push(key, value, index1, key1, index2, key2, index3, key3);
}
public void putAll (IntFloatMap map) {
for (Entry entry : map.entries())
put(entry.key, entry.value);
}
/** Skips checks for existing keys. */
private void putResize (int key, float value) {
if (key == 0) {
zeroValue = value;
hasZeroValue = true;
return;
}
// Check for empty buckets.
int index1 = key & mask;
int key1 = keyTable[index1];
if (key1 == EMPTY) {
keyTable[index1] = key;
valueTable[index1] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
int index2 = hash2(key);
int key2 = keyTable[index2];
if (key2 == EMPTY) {
keyTable[index2] = key;
valueTable[index2] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
int index3 = hash3(key);
int key3 = keyTable[index3];
if (key3 == EMPTY) {
keyTable[index3] = key;
valueTable[index3] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
push(key, value, index1, key1, index2, key2, index3, key3);
}
private void push (int insertKey, float insertValue, int index1, int key1, int index2, int key2, int index3, int key3) {
int[] keyTable = this.keyTable;
float[] valueTable = this.valueTable;
int mask = this.mask;
// Push keys until an empty bucket is found.
int evictedKey;
float evictedValue;
int i = 0, pushIterations = this.pushIterations;
do {
// Replace the key and value for one of the hashes.
switch (MathUtil.random(2)) {
case 0:
evictedKey = key1;
evictedValue = valueTable[index1];
keyTable[index1] = insertKey;
valueTable[index1] = insertValue;
break;
case 1:
evictedKey = key2;
evictedValue = valueTable[index2];
keyTable[index2] = insertKey;
valueTable[index2] = insertValue;
break;
default:
evictedKey = key3;
evictedValue = valueTable[index3];
keyTable[index3] = insertKey;
valueTable[index3] = insertValue;
break;
}
// If the evicted key hashes to an empty bucket, put it there and stop.
index1 = evictedKey & mask;
key1 = keyTable[index1];
if (key1 == EMPTY) {
keyTable[index1] = evictedKey;
valueTable[index1] = evictedValue;
if (size++ >= threshold) resize(capacity << 1);
return;
}
index2 = hash2(evictedKey);
key2 = keyTable[index2];
if (key2 == EMPTY) {
keyTable[index2] = evictedKey;
valueTable[index2] = evictedValue;
if (size++ >= threshold) resize(capacity << 1);
return;
}
index3 = hash3(evictedKey);
key3 = keyTable[index3];
if (key3 == EMPTY) {
keyTable[index3] = evictedKey;
valueTable[index3] = evictedValue;
if (size++ >= threshold) resize(capacity << 1);
return;
}
if (++i == pushIterations) break;
insertKey = evictedKey;
insertValue = evictedValue;
} while (true);
putStash(evictedKey, evictedValue);
}
private void putStash (int key, float value) {
if (stashSize == stashCapacity) {
// Too many pushes occurred and the stash is full, increase the table size.
resize(capacity << 1);
putResize(key, value);
return;
}
// Store key in the stash.
int index = capacity + stashSize;
keyTable[index] = key;
valueTable[index] = value;
stashSize++;
size++;
}
/** @param defaultValue Returned if the key was not associated with a value. */
public float get (int key, float defaultValue) {
if (key == 0) {
if (!hasZeroValue) return defaultValue;
return zeroValue;
}
int index = key & mask;
if (keyTable[index] != key) {
index = hash2(key);
if (keyTable[index] != key) {
index = hash3(key);
if (keyTable[index] != key) return getStash(key, defaultValue);
}
}
return valueTable[index];
}
private float getStash (int key, float defaultValue) {
int[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++)
if (key == keyTable[i]) return valueTable[i];
return defaultValue;
}
/** Returns the key's current value and increments the stored value. If the key is not in the map, defaultValue + increment is
* put into the map. */
public float getAndIncrement (int key, float defaultValue, float increment) {
if (key == 0) {
if (hasZeroValue) {
float value = zeroValue;
zeroValue += increment;
return value;
} else {
hasZeroValue = true;
zeroValue = defaultValue + increment;
++size;
return defaultValue;
}
}
int index = key & mask;
if (key != keyTable[index]) {
index = hash2(key);
if (key != keyTable[index]) {
index = hash3(key);
if (key != keyTable[index]) return getAndIncrementStash(key, defaultValue, increment);
}
}
float value = valueTable[index];
valueTable[index] = value + increment;
return value;
}
private float getAndIncrementStash (int key, float defaultValue, float increment) {
int[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++)
if (key == keyTable[i]) {
float value = valueTable[i];
valueTable[i] = value + increment;
return value;
}
put(key, defaultValue + increment);
return defaultValue;
}
public float remove (int key, float defaultValue) {
if (key == 0) {
if (!hasZeroValue) return defaultValue;
hasZeroValue = false;
size--;
return zeroValue;
}
int index = key & mask;
if (key == keyTable[index]) {
keyTable[index] = EMPTY;
float oldValue = valueTable[index];
size--;
return oldValue;
}
index = hash2(key);
if (key == keyTable[index]) {
keyTable[index] = EMPTY;
float oldValue = valueTable[index];
size--;
return oldValue;
}
index = hash3(key);
if (key == keyTable[index]) {
keyTable[index] = EMPTY;
float oldValue = valueTable[index];
size--;
return oldValue;
}
return removeStash(key, defaultValue);
}
float removeStash (int key, float defaultValue) {
int[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++) {
if (key == keyTable[i]) {
float oldValue = valueTable[i];
removeStashIndex(i);
size--;
return oldValue;
}
}
return defaultValue;
}
void removeStashIndex (int index) {
// If the removed location was not last, move the last tuple to the removed location.
stashSize--;
int lastIndex = capacity + stashSize;
if (index < lastIndex) {
keyTable[index] = keyTable[lastIndex];
valueTable[index] = valueTable[lastIndex];
}
}
/** Returns true if the map is empty. */
public boolean isEmpty () {
return size == 0;
}
/** Reduces the size of the backing arrays to be the specified capacity or less. If the capacity is already less, nothing is
* done. If the map contains more items than the specified capacity, the next highest power of two capacity is used instead. */
public void shrink (int maximumCapacity) {
if (maximumCapacity < 0) throw new IllegalArgumentException("maximumCapacity must be >= 0: " + maximumCapacity);
if (size > maximumCapacity) maximumCapacity = size;
if (capacity <= maximumCapacity) return;
maximumCapacity = MathUtil.nextPowerOfTwo(maximumCapacity);
resize(maximumCapacity);
}
/** Clears the map and reduces the size of the backing arrays to be the specified capacity if they are larger. */
public void clear (int maximumCapacity) {
if (capacity <= maximumCapacity) {
clear();
return;
}
hasZeroValue = false;
size = 0;
resize(maximumCapacity);
}
public void clear () {
if (size == 0) return;
int[] keyTable = this.keyTable;
for (int i = capacity + stashSize; i-- > 0;)
keyTable[i] = EMPTY;
hasZeroValue = false;
size = 0;
stashSize = 0;
}
/** Returns true if the specified value is in the map. Note this traverses the entire map and compares every value, which may be
* an expensive operation. */
public boolean containsValue (float value) {
if (hasZeroValue && zeroValue == value) return true;
int[] keyTable = this.keyTable;
float[] valueTable = this.valueTable;
for (int i = capacity + stashSize; i-- > 0;)
if (keyTable[i] != 0 && valueTable[i] == value) return true;
return false;
}
/** Returns true if the specified value is in the map. Note this traverses the entire map and compares every value, which may be
* an expensive operation. */
public boolean containsValue (float value, float epsilon) {
if (hasZeroValue && Math.abs(zeroValue - value) <= epsilon) return true;
float[] valueTable = this.valueTable;
for (int i = capacity + stashSize; i-- > 0;)
if (Math.abs(valueTable[i] - value) <= epsilon) return true;
return false;
}
public boolean containsKey (int key) {
if (key == 0) return hasZeroValue;
int index = key & mask;
if (keyTable[index] != key) {
index = hash2(key);
if (keyTable[index] != key) {
index = hash3(key);
if (keyTable[index] != key) return containsKeyStash(key);
}
}
return true;
}
private boolean containsKeyStash (int key) {
int[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++)
if (key == keyTable[i]) return true;
return false;
}
/** Returns the key for the specified value, or null if it is not in the map. Note this traverses the entire map and compares
* every value, which may be an expensive operation. */
public int findKey (float value, int notFound) {
if (hasZeroValue && zeroValue == value) return 0;
int[] keyTable = this.keyTable;
float[] valueTable = this.valueTable;
for (int i = capacity + stashSize; i-- > 0;)
if (keyTable[i] != 0 && valueTable[i] == value) return keyTable[i];
return notFound;
}
/** Increases the size of the backing array to accommodate the specified number of additional items. Useful before adding many
* items to avoid multiple backing array resizes. */
public void ensureCapacity (int additionalCapacity) {
if (additionalCapacity < 0) throw new IllegalArgumentException("additionalCapacity must be >= 0: " + additionalCapacity);
int sizeNeeded = size + additionalCapacity;
if (sizeNeeded >= threshold) resize(MathUtil.nextPowerOfTwo((int)Math.ceil(sizeNeeded / loadFactor)));
}
private void resize (int newSize) {
int oldEndIndex = capacity + stashSize;
capacity = newSize;
threshold = (int)(newSize * loadFactor);
mask = newSize - 1;
hashShift = 31 - Integer.numberOfTrailingZeros(newSize);
stashCapacity = Math.max(3, (int)Math.ceil(Math.log(newSize)) * 2);
pushIterations = Math.max(Math.min(newSize, 8), (int)Math.sqrt(newSize) / 8);
int[] oldKeyTable = keyTable;
float[] oldValueTable = valueTable;
keyTable = new int[newSize + stashCapacity];
valueTable = new float[newSize + stashCapacity];
int oldSize = size;
size = hasZeroValue ? 1 : 0;
stashSize = 0;
if (oldSize > 0) {
for (int i = 0; i < oldEndIndex; i++) {
int key = oldKeyTable[i];
if (key != EMPTY) putResize(key, oldValueTable[i]);
}
}
}
private int hash2 (int h) {
h *= PRIME2;
return (h ^ h >>> hashShift) & mask;
}
private int hash3 (int h) {
h *= PRIME3;
return (h ^ h >>> hashShift) & mask;
}
public int hashCode () {
int h = 0;
if (hasZeroValue) {
h += Float.floatToIntBits(zeroValue);
}
int[] keyTable = this.keyTable;
float[] valueTable = this.valueTable;
for (int i = 0, n = capacity + stashSize; i < n; i++) {
int key = keyTable[i];
if (key != EMPTY) {
h += key * 31;
float value = valueTable[i];
h += Float.floatToIntBits(value);
}
}
return h;
}
public boolean equals (Object obj) {
if (obj == this) return true;
if (!(obj instanceof IntFloatMap)) return false;
IntFloatMap other = (IntFloatMap)obj;
if (other.size != size) return false;
if (other.hasZeroValue != hasZeroValue) return false;
if (hasZeroValue && other.zeroValue != zeroValue) {
return false;
}
int[] keyTable = this.keyTable;
float[] valueTable = this.valueTable;
for (int i = 0, n = capacity + stashSize; i < n; i++) {
int key = keyTable[i];
if (key != EMPTY) {
float otherValue = other.get(key, 0f);
if (otherValue == 0f && !other.containsKey(key)) return false;
float value = valueTable[i];
if (otherValue != value) return false;
}
}
return true;
}
public String toString () {
if (size == 0) return "{}";
StringBuilder buffer = new StringBuilder(32);
buffer.append('{');
int[] keyTable = this.keyTable;
float[] valueTable = this.valueTable;
int i = keyTable.length;
if (hasZeroValue) {
buffer.append("0=");
buffer.append(zeroValue);
} else {
while (i-- > 0) {
int key = keyTable[i];
if (key == EMPTY) continue;
buffer.append(key);
buffer.append('=');
buffer.append(valueTable[i]);
break;
}
}
while (i-- > 0) {
int key = keyTable[i];
if (key == EMPTY) continue;
buffer.append(", ");
buffer.append(key);
buffer.append('=');
buffer.append(valueTable[i]);
}
buffer.append('}');
return buffer.toString();
}
public Iterator<Entry> iterator () {
return entries();
}
/** Returns an iterator for the entries in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */
public Entries entries () {
if (entries1 == null) {
entries1 = new Entries(this);
entries2 = new Entries(this);
}
if (!entries1.valid) {
entries1.reset();
entries1.valid = true;
entries2.valid = false;
return entries1;
}
entries2.reset();
entries2.valid = true;
entries1.valid = false;
return entries2;
}
/** Returns an iterator for the values in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */
public Values values () {
if (values1 == null) {
values1 = new Values(this);
values2 = new Values(this);
}
if (!values1.valid) {
values1.reset();
values1.valid = true;
values2.valid = false;
return values1;
}
values2.reset();
values2.valid = true;
values1.valid = false;
return values2;
}
/** Returns an iterator for the keys in the map. Remove is supported. Note that the same iterator instance is returned each time
* this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */
public Keys keys () {
if (keys1 == null) {
keys1 = new Keys(this);
keys2 = new Keys(this);
}
if (!keys1.valid) {
keys1.reset();
keys1.valid = true;
keys2.valid = false;
return keys1;
}
keys2.reset();
keys2.valid = true;
keys1.valid = false;
return keys2;
}
static public class Entry {
public int key;
public float value;
public String toString () {
return key + "=" + value;
}
}
static private class MapIterator {
static final int INDEX_ILLEGAL = -2;
static final int INDEX_ZERO = -1;
public boolean hasNext;
final IntFloatMap map;
int nextIndex, currentIndex;
boolean valid = true;
public MapIterator (IntFloatMap map) {
this.map = map;
reset();
}
public void reset () {
currentIndex = INDEX_ILLEGAL;
nextIndex = INDEX_ZERO;
if (map.hasZeroValue)
hasNext = true;
else
findNextIndex();
}
void findNextIndex () {
hasNext = false;
int[] keyTable = map.keyTable;
for (int n = map.capacity + map.stashSize; ++nextIndex < n;) {
if (keyTable[nextIndex] != EMPTY) {
hasNext = true;
break;
}
}
}
public void remove () {
if (currentIndex == INDEX_ZERO && map.hasZeroValue) {
map.hasZeroValue = false;
} else if (currentIndex < 0) {
throw new IllegalStateException("next must be called before remove.");
} else if (currentIndex >= map.capacity) {
map.removeStashIndex(currentIndex);
nextIndex = currentIndex - 1;
findNextIndex();
} else {
map.keyTable[currentIndex] = EMPTY;
}
currentIndex = INDEX_ILLEGAL;
map.size--;
}
}
static public class Entries extends MapIterator implements Iterable<Entry>, Iterator<Entry> {
private Entry entry = new Entry();
public Entries (IntFloatMap map) {
super(map);
}
/** Note the same entry instance is returned each time this method is called. */
public Entry next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
int[] keyTable = map.keyTable;
if (nextIndex == INDEX_ZERO) {
entry.key = 0;
entry.value = map.zeroValue;
} else {
entry.key = keyTable[nextIndex];
entry.value = map.valueTable[nextIndex];
}
currentIndex = nextIndex;
findNextIndex();
return entry;
}
public boolean hasNext () {
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
return hasNext;
}
public Iterator<Entry> iterator () {
return this;
}
public void remove () {
super.remove();
}
}
static public class Values extends MapIterator {
public Values (IntFloatMap map) {
super(map);
}
public boolean hasNext () {
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
return hasNext;
}
public float next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
float value;
if (nextIndex == INDEX_ZERO)
value = map.zeroValue;
else
value = map.valueTable[nextIndex];
currentIndex = nextIndex;
findNextIndex();
return value;
}
/** Returns a new array containing the remaining values. */
public FloatArray toArray () {
FloatArray array = new FloatArray(true, map.size);
while (hasNext)
array.add(next());
return array;
}
}
static public class Keys extends MapIterator {
public Keys (IntFloatMap map) {
super(map);
}
public boolean hasNext () {
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
return hasNext;
}
public int next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
int key = nextIndex == INDEX_ZERO ? 0 : map.keyTable[nextIndex];
currentIndex = nextIndex;
findNextIndex();
return key;
}
/** Returns a new array containing the remaining keys. */
public IntArray toArray () {
IntArray array = new IntArray(true, map.size);
while (hasNext)
array.add(next());
return array;
}
}
}

View File

@ -0,0 +1,828 @@
/*******************************************************************************
* Copyright 2011 LibGDX.
* Mario Zechner <badlogicgames@gmail.com>
* Nathan Sweet <nathan.sweet@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package dorkbox.collections;
import java.util.Iterator;
import java.util.NoSuchElementException;
/** An unordered map where the keys and values are ints. This implementation is a cuckoo hash map using 3 hashes, random walking,
* and a small stash for problematic keys. No allocation is done except when growing the table size. <br>
* <br>
* This map performs very fast get, containsKey, and remove (typically O(1), worst case O(log(n))). Put may be a bit slower,
* depending on hash collisions. Load factors greater than 0.91 greatly increase the chances the map will have to rehash to the
* next higher POT size.
* @author Nathan Sweet */
public class IntIntMap implements Iterable<IntIntMap.Entry> {
private static final int PRIME1 = 0xbe1f14b1;
private static final int PRIME2 = 0xb4b82e39;
private static final int PRIME3 = 0xced1c241;
private static final int EMPTY = 0;
public int size;
int[] keyTable, valueTable;
int capacity, stashSize;
int zeroValue;
boolean hasZeroValue;
private float loadFactor;
private int hashShift, mask, threshold;
private int stashCapacity;
private int pushIterations;
private Entries entries1, entries2;
private Values values1, values2;
private Keys keys1, keys2;
/** Creates a new map with an initial capacity of 51 and a load factor of 0.8. */
public IntIntMap () {
this(51, 0.8f);
}
/** Creates a new map with a load factor of 0.8.
* @param initialCapacity If not a power of two, it is increased to the next nearest power of two. */
public IntIntMap (int initialCapacity) {
this(initialCapacity, 0.8f);
}
/** Creates a new map with the specified initial capacity and load factor. This map will hold initialCapacity items before
* growing the backing table.
* @param initialCapacity If not a power of two, it is increased to the next nearest power of two. */
public IntIntMap (int initialCapacity, float loadFactor) {
if (initialCapacity < 0) throw new IllegalArgumentException("initialCapacity must be >= 0: " + initialCapacity);
initialCapacity = MathUtil.nextPowerOfTwo((int)Math.ceil(initialCapacity / loadFactor));
if (initialCapacity > 1 << 30) throw new IllegalArgumentException("initialCapacity is too large: " + initialCapacity);
capacity = initialCapacity;
if (loadFactor <= 0) throw new IllegalArgumentException("loadFactor must be > 0: " + loadFactor);
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
mask = capacity - 1;
hashShift = 31 - Integer.numberOfTrailingZeros(capacity);
stashCapacity = Math.max(3, (int)Math.ceil(Math.log(capacity)) * 2);
pushIterations = Math.max(Math.min(capacity, 8), (int)Math.sqrt(capacity) / 8);
keyTable = new int[capacity + stashCapacity];
valueTable = new int[keyTable.length];
}
/** Creates a new map identical to the specified map. */
public IntIntMap (IntIntMap map) {
this((int)Math.floor(map.capacity * map.loadFactor), map.loadFactor);
stashSize = map.stashSize;
System.arraycopy(map.keyTable, 0, keyTable, 0, map.keyTable.length);
System.arraycopy(map.valueTable, 0, valueTable, 0, map.valueTable.length);
size = map.size;
zeroValue = map.zeroValue;
hasZeroValue = map.hasZeroValue;
}
public void put (int key, int value) {
if (key == 0) {
zeroValue = value;
if (!hasZeroValue) {
hasZeroValue = true;
size++;
}
return;
}
int[] keyTable = this.keyTable;
// Check for existing keys.
int index1 = key & mask;
int key1 = keyTable[index1];
if (key == key1) {
valueTable[index1] = value;
return;
}
int index2 = hash2(key);
int key2 = keyTable[index2];
if (key == key2) {
valueTable[index2] = value;
return;
}
int index3 = hash3(key);
int key3 = keyTable[index3];
if (key == key3) {
valueTable[index3] = value;
return;
}
// Update key in the stash.
for (int i = capacity, n = i + stashSize; i < n; i++) {
if (key == keyTable[i]) {
valueTable[i] = value;
return;
}
}
// Check for empty buckets.
if (key1 == EMPTY) {
keyTable[index1] = key;
valueTable[index1] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
if (key2 == EMPTY) {
keyTable[index2] = key;
valueTable[index2] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
if (key3 == EMPTY) {
keyTable[index3] = key;
valueTable[index3] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
push(key, value, index1, key1, index2, key2, index3, key3);
}
public void putAll (IntIntMap map) {
for (Entry entry : map.entries())
put(entry.key, entry.value);
}
/** Skips checks for existing keys. */
private void putResize (int key, int value) {
if (key == 0) {
zeroValue = value;
hasZeroValue = true;
return;
}
// Check for empty buckets.
int index1 = key & mask;
int key1 = keyTable[index1];
if (key1 == EMPTY) {
keyTable[index1] = key;
valueTable[index1] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
int index2 = hash2(key);
int key2 = keyTable[index2];
if (key2 == EMPTY) {
keyTable[index2] = key;
valueTable[index2] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
int index3 = hash3(key);
int key3 = keyTable[index3];
if (key3 == EMPTY) {
keyTable[index3] = key;
valueTable[index3] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
push(key, value, index1, key1, index2, key2, index3, key3);
}
private void push (int insertKey, int insertValue, int index1, int key1, int index2, int key2, int index3, int key3) {
int[] keyTable = this.keyTable;
int[] valueTable = this.valueTable;
int mask = this.mask;
// Push keys until an empty bucket is found.
int evictedKey;
int evictedValue;
int i = 0, pushIterations = this.pushIterations;
do {
// Replace the key and value for one of the hashes.
switch (MathUtil.random(2)) {
case 0:
evictedKey = key1;
evictedValue = valueTable[index1];
keyTable[index1] = insertKey;
valueTable[index1] = insertValue;
break;
case 1:
evictedKey = key2;
evictedValue = valueTable[index2];
keyTable[index2] = insertKey;
valueTable[index2] = insertValue;
break;
default:
evictedKey = key3;
evictedValue = valueTable[index3];
keyTable[index3] = insertKey;
valueTable[index3] = insertValue;
break;
}
// If the evicted key hashes to an empty bucket, put it there and stop.
index1 = evictedKey & mask;
key1 = keyTable[index1];
if (key1 == EMPTY) {
keyTable[index1] = evictedKey;
valueTable[index1] = evictedValue;
if (size++ >= threshold) resize(capacity << 1);
return;
}
index2 = hash2(evictedKey);
key2 = keyTable[index2];
if (key2 == EMPTY) {
keyTable[index2] = evictedKey;
valueTable[index2] = evictedValue;
if (size++ >= threshold) resize(capacity << 1);
return;
}
index3 = hash3(evictedKey);
key3 = keyTable[index3];
if (key3 == EMPTY) {
keyTable[index3] = evictedKey;
valueTable[index3] = evictedValue;
if (size++ >= threshold) resize(capacity << 1);
return;
}
if (++i == pushIterations) break;
insertKey = evictedKey;
insertValue = evictedValue;
} while (true);
putStash(evictedKey, evictedValue);
}
private void putStash (int key, int value) {
if (stashSize == stashCapacity) {
// Too many pushes occurred and the stash is full, increase the table size.
resize(capacity << 1);
putResize(key, value);
return;
}
// Store key in the stash.
int index = capacity + stashSize;
keyTable[index] = key;
valueTable[index] = value;
stashSize++;
size++;
}
/** @param defaultValue Returned if the key was not associated with a value. */
public int get (int key, int defaultValue) {
if (key == 0) {
if (!hasZeroValue) return defaultValue;
return zeroValue;
}
int index = key & mask;
if (keyTable[index] != key) {
index = hash2(key);
if (keyTable[index] != key) {
index = hash3(key);
if (keyTable[index] != key) return getStash(key, defaultValue);
}
}
return valueTable[index];
}
private int getStash (int key, int defaultValue) {
int[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++)
if (key == keyTable[i]) return valueTable[i];
return defaultValue;
}
/** Returns the key's current value and increments the stored value. If the key is not in the map, defaultValue + increment is
* put into the map. */
public int getAndIncrement (int key, int defaultValue, int increment) {
if (key == 0) {
if (hasZeroValue) {
int value = zeroValue;
zeroValue += increment;
return value;
} else {
hasZeroValue = true;
zeroValue = defaultValue + increment;
++size;
return defaultValue;
}
}
int index = key & mask;
if (key != keyTable[index]) {
index = hash2(key);
if (key != keyTable[index]) {
index = hash3(key);
if (key != keyTable[index]) return getAndIncrementStash(key, defaultValue, increment);
}
}
int value = valueTable[index];
valueTable[index] = value + increment;
return value;
}
private int getAndIncrementStash (int key, int defaultValue, int increment) {
int[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++)
if (key == keyTable[i]) {
int value = valueTable[i];
valueTable[i] = value + increment;
return value;
}
put(key, defaultValue + increment);
return defaultValue;
}
public int remove (int key, int defaultValue) {
if (key == 0) {
if (!hasZeroValue) return defaultValue;
hasZeroValue = false;
size--;
return zeroValue;
}
int index = key & mask;
if (key == keyTable[index]) {
keyTable[index] = EMPTY;
int oldValue = valueTable[index];
size--;
return oldValue;
}
index = hash2(key);
if (key == keyTable[index]) {
keyTable[index] = EMPTY;
int oldValue = valueTable[index];
size--;
return oldValue;
}
index = hash3(key);
if (key == keyTable[index]) {
keyTable[index] = EMPTY;
int oldValue = valueTable[index];
size--;
return oldValue;
}
return removeStash(key, defaultValue);
}
int removeStash (int key, int defaultValue) {
int[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++) {
if (key == keyTable[i]) {
int oldValue = valueTable[i];
removeStashIndex(i);
size--;
return oldValue;
}
}
return defaultValue;
}
void removeStashIndex (int index) {
// If the removed location was not last, move the last tuple to the removed location.
stashSize--;
int lastIndex = capacity + stashSize;
if (index < lastIndex) {
keyTable[index] = keyTable[lastIndex];
valueTable[index] = valueTable[lastIndex];
}
}
/** Returns true if the map is empty. */
public boolean isEmpty () {
return size == 0;
}
/** Reduces the size of the backing arrays to be the specified capacity or less. If the capacity is already less, nothing is
* done. If the map contains more items than the specified capacity, the next highest power of two capacity is used instead. */
public void shrink (int maximumCapacity) {
if (maximumCapacity < 0) throw new IllegalArgumentException("maximumCapacity must be >= 0: " + maximumCapacity);
if (size > maximumCapacity) maximumCapacity = size;
if (capacity <= maximumCapacity) return;
maximumCapacity = MathUtil.nextPowerOfTwo(maximumCapacity);
resize(maximumCapacity);
}
/** Clears the map and reduces the size of the backing arrays to be the specified capacity if they are larger. */
public void clear (int maximumCapacity) {
if (capacity <= maximumCapacity) {
clear();
return;
}
hasZeroValue = false;
size = 0;
resize(maximumCapacity);
}
public void clear () {
if (size == 0) return;
int[] keyTable = this.keyTable;
for (int i = capacity + stashSize; i-- > 0;)
keyTable[i] = EMPTY;
size = 0;
stashSize = 0;
hasZeroValue = false;
}
/** Returns true if the specified value is in the map. Note this traverses the entire map and compares every value, which may be
* an expensive operation. */
public boolean containsValue (int value) {
if (hasZeroValue && zeroValue == value) return true;
int[] keyTable = this.keyTable, valueTable = this.valueTable;
for (int i = capacity + stashSize; i-- > 0;)
if (keyTable[i] != 0 && valueTable[i] == value) return true;
return false;
}
public boolean containsKey (int key) {
if (key == 0) return hasZeroValue;
int index = key & mask;
if (keyTable[index] != key) {
index = hash2(key);
if (keyTable[index] != key) {
index = hash3(key);
if (keyTable[index] != key) return containsKeyStash(key);
}
}
return true;
}
private boolean containsKeyStash (int key) {
int[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++)
if (key == keyTable[i]) return true;
return false;
}
/** Returns the key for the specified value, or null if it is not in the map. Note this traverses the entire map and compares
* every value, which may be an expensive operation. */
public int findKey (int value, int notFound) {
if (hasZeroValue && zeroValue == value) return 0;
int[] keyTable = this.keyTable, valueTable = this.valueTable;
for (int i = capacity + stashSize; i-- > 0;)
if (keyTable[i] != 0 && valueTable[i] == value) return keyTable[i];
return notFound;
}
/** Increases the size of the backing array to accommodate the specified number of additional items. Useful before adding many
* items to avoid multiple backing array resizes. */
public void ensureCapacity (int additionalCapacity) {
if (additionalCapacity < 0) throw new IllegalArgumentException("additionalCapacity must be >= 0: " + additionalCapacity);
int sizeNeeded = size + additionalCapacity;
if (sizeNeeded >= threshold) resize(MathUtil.nextPowerOfTwo((int)Math.ceil(sizeNeeded / loadFactor)));
}
private void resize (int newSize) {
int oldEndIndex = capacity + stashSize;
capacity = newSize;
threshold = (int)(newSize * loadFactor);
mask = newSize - 1;
hashShift = 31 - Integer.numberOfTrailingZeros(newSize);
stashCapacity = Math.max(3, (int)Math.ceil(Math.log(newSize)) * 2);
pushIterations = Math.max(Math.min(newSize, 8), (int)Math.sqrt(newSize) / 8);
int[] oldKeyTable = keyTable;
int[] oldValueTable = valueTable;
keyTable = new int[newSize + stashCapacity];
valueTable = new int[newSize + stashCapacity];
int oldSize = size;
size = hasZeroValue ? 1 : 0;
stashSize = 0;
if (oldSize > 0) {
for (int i = 0; i < oldEndIndex; i++) {
int key = oldKeyTable[i];
if (key != EMPTY) putResize(key, oldValueTable[i]);
}
}
}
private int hash2 (int h) {
h *= PRIME2;
return (h ^ h >>> hashShift) & mask;
}
private int hash3 (int h) {
h *= PRIME3;
return (h ^ h >>> hashShift) & mask;
}
public int hashCode () {
int h = 0;
if (hasZeroValue) {
h += Float.floatToIntBits(zeroValue);
}
int[] keyTable = this.keyTable;
int[] valueTable = this.valueTable;
for (int i = 0, n = capacity + stashSize; i < n; i++) {
int key = keyTable[i];
if (key != EMPTY) {
h += key * 31;
int value = valueTable[i];
h += value;
}
}
return h;
}
public boolean equals (Object obj) {
if (obj == this) return true;
if (!(obj instanceof IntIntMap)) return false;
IntIntMap other = (IntIntMap)obj;
if (other.size != size) return false;
if (other.hasZeroValue != hasZeroValue) return false;
if (hasZeroValue && other.zeroValue != zeroValue) {
return false;
}
int[] keyTable = this.keyTable;
int[] valueTable = this.valueTable;
for (int i = 0, n = capacity + stashSize; i < n; i++) {
int key = keyTable[i];
if (key != EMPTY) {
int otherValue = other.get(key, 0);
if (otherValue == 0 && !other.containsKey(key)) return false;
int value = valueTable[i];
if (otherValue != value) return false;
}
}
return true;
}
public String toString () {
if (size == 0) return "{}";
StringBuilder buffer = new StringBuilder(32);
buffer.append('{');
int[] keyTable = this.keyTable;
int[] valueTable = this.valueTable;
int i = keyTable.length;
if (hasZeroValue) {
buffer.append("0=");
buffer.append(zeroValue);
} else {
while (i-- > 0) {
int key = keyTable[i];
if (key == EMPTY) continue;
buffer.append(key);
buffer.append('=');
buffer.append(valueTable[i]);
break;
}
}
while (i-- > 0) {
int key = keyTable[i];
if (key == EMPTY) continue;
buffer.append(", ");
buffer.append(key);
buffer.append('=');
buffer.append(valueTable[i]);
}
buffer.append('}');
return buffer.toString();
}
public Iterator<Entry> iterator () {
return entries();
}
/** Returns an iterator for the entries in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */
public Entries entries () {
if (entries1 == null) {
entries1 = new Entries(this);
entries2 = new Entries(this);
}
if (!entries1.valid) {
entries1.reset();
entries1.valid = true;
entries2.valid = false;
return entries1;
}
entries2.reset();
entries2.valid = true;
entries1.valid = false;
return entries2;
}
/** Returns an iterator for the values in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */
public Values values () {
if (values1 == null) {
values1 = new Values(this);
values2 = new Values(this);
}
if (!values1.valid) {
values1.reset();
values1.valid = true;
values2.valid = false;
return values1;
}
values2.reset();
values2.valid = true;
values1.valid = false;
return values2;
}
/** Returns an iterator for the keys in the map. Remove is supported. Note that the same iterator instance is returned each time
* this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */
public Keys keys () {
if (keys1 == null) {
keys1 = new Keys(this);
keys2 = new Keys(this);
}
if (!keys1.valid) {
keys1.reset();
keys1.valid = true;
keys2.valid = false;
return keys1;
}
keys2.reset();
keys2.valid = true;
keys1.valid = false;
return keys2;
}
static public class Entry {
public int key;
public int value;
public String toString () {
return key + "=" + value;
}
}
static private class MapIterator {
static final int INDEX_ILLEGAL = -2;
static final int INDEX_ZERO = -1;
public boolean hasNext;
final IntIntMap map;
int nextIndex, currentIndex;
boolean valid = true;
public MapIterator (IntIntMap map) {
this.map = map;
reset();
}
public void reset () {
currentIndex = INDEX_ILLEGAL;
nextIndex = INDEX_ZERO;
if (map.hasZeroValue)
hasNext = true;
else
findNextIndex();
}
void findNextIndex () {
hasNext = false;
int[] keyTable = map.keyTable;
for (int n = map.capacity + map.stashSize; ++nextIndex < n;) {
if (keyTable[nextIndex] != EMPTY) {
hasNext = true;
break;
}
}
}
public void remove () {
if (currentIndex == INDEX_ZERO && map.hasZeroValue) {
map.hasZeroValue = false;
} else if (currentIndex < 0) {
throw new IllegalStateException("next must be called before remove.");
} else if (currentIndex >= map.capacity) {
map.removeStashIndex(currentIndex);
nextIndex = currentIndex - 1;
findNextIndex();
} else {
map.keyTable[currentIndex] = EMPTY;
}
currentIndex = INDEX_ILLEGAL;
map.size--;
}
}
static public class Entries extends MapIterator implements Iterable<Entry>, Iterator<Entry> {
private Entry entry = new Entry();
public Entries (IntIntMap map) {
super(map);
}
/** Note the same entry instance is returned each time this method is called. */
public Entry next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
int[] keyTable = map.keyTable;
if (nextIndex == INDEX_ZERO) {
entry.key = 0;
entry.value = map.zeroValue;
} else {
entry.key = keyTable[nextIndex];
entry.value = map.valueTable[nextIndex];
}
currentIndex = nextIndex;
findNextIndex();
return entry;
}
public boolean hasNext () {
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
return hasNext;
}
public Iterator<Entry> iterator () {
return this;
}
public void remove () {
super.remove();
}
}
static public class Values extends MapIterator {
public Values (IntIntMap map) {
super(map);
}
public boolean hasNext () {
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
return hasNext;
}
public int next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
int value;
if (nextIndex == INDEX_ZERO)
value = map.zeroValue;
else
value = map.valueTable[nextIndex];
currentIndex = nextIndex;
findNextIndex();
return value;
}
/** Returns a new array containing the remaining values. */
public IntArray toArray () {
IntArray array = new IntArray(true, map.size);
while (hasNext)
array.add(next());
return array;
}
}
static public class Keys extends MapIterator {
public Keys (IntIntMap map) {
super(map);
}
public boolean hasNext () {
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
return hasNext;
}
public int next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
int key = nextIndex == INDEX_ZERO ? 0 : map.keyTable[nextIndex];
currentIndex = nextIndex;
findNextIndex();
return key;
}
/** Returns a new array containing the remaining keys. */
public IntArray toArray () {
IntArray array = new IntArray(true, map.size);
while (hasNext)
array.add(next());
return array;
}
}
}

View File

@ -0,0 +1,881 @@
/*******************************************************************************
* Copyright 2011 LibGDX.
* Mario Zechner <badlogicgames@gmail.com>
* Nathan Sweet <nathan.sweet@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package dorkbox.collections;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Random;
/** An unordered map that uses int keys. This implementation is a cuckoo hash map using 3 hashes, random walking, and a small
* stash for problematic keys. Null values are allowed. No allocation is done except when growing the table size. <br>
* <br>
* This map performs very fast get, containsKey, and remove (typically O(1), worst case O(log(n))). Put may be a bit slower,
* depending on hash collisions. Load factors greater than 0.91 greatly increase the chances the map will have to rehash to the
* next higher POT size.
* @author Nathan Sweet */
@SuppressWarnings({"unchecked", "rawtypes"})
public class IntMap<V> implements Iterable<IntMap.Entry<V>> {
private static final int PRIME1 = 0xbe1f14b1;
private static final int PRIME2 = 0xb4b82e39;
private static final int PRIME3 = 0xced1c241;
private static final int EMPTY = 0;
public int size;
int[] keyTable;
V[] valueTable;
int capacity, stashSize;
V zeroValue;
boolean hasZeroValue;
private float loadFactor;
private int hashShift, mask, threshold;
private int stashCapacity;
private int pushIterations;
private Entries entries1, entries2;
private Values values1, values2;
private Keys keys1, keys2;
/** Creates a new map with an initial capacity of 51 and a load factor of 0.8. */
public IntMap () {
this(51, 0.8f);
}
/** Creates a new map with a load factor of 0.8.
* @param initialCapacity If not a power of two, it is increased to the next nearest power of two. */
public IntMap (int initialCapacity) {
this(initialCapacity, 0.8f);
}
/** Creates a new map with the specified initial capacity and load factor. This map will hold initialCapacity items before
* growing the backing table.
* @param initialCapacity If not a power of two, it is increased to the next nearest power of two. */
public IntMap (int initialCapacity, float loadFactor) {
if (initialCapacity < 0) throw new IllegalArgumentException("initialCapacity must be >= 0: " + initialCapacity);
initialCapacity = MathUtil.nextPowerOfTwo((int)Math.ceil(initialCapacity / loadFactor));
if (initialCapacity > 1 << 30) throw new IllegalArgumentException("initialCapacity is too large: " + initialCapacity);
capacity = initialCapacity;
if (loadFactor <= 0) throw new IllegalArgumentException("loadFactor must be > 0: " + loadFactor);
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
mask = capacity - 1;
hashShift = 31 - Integer.numberOfTrailingZeros(capacity);
stashCapacity = Math.max(3, (int)Math.ceil(Math.log(capacity)) * 2);
pushIterations = Math.max(Math.min(capacity, 8), (int)Math.sqrt(capacity) / 8);
keyTable = new int[capacity + stashCapacity];
valueTable = (V[])new Object[keyTable.length];
}
/** Creates a new map identical to the specified map. */
public IntMap (IntMap<? extends V> map) {
this((int)Math.floor(map.capacity * map.loadFactor), map.loadFactor);
stashSize = map.stashSize;
System.arraycopy(map.keyTable, 0, keyTable, 0, map.keyTable.length);
System.arraycopy(map.valueTable, 0, valueTable, 0, map.valueTable.length);
size = map.size;
zeroValue = map.zeroValue;
hasZeroValue = map.hasZeroValue;
}
public V put (int key, V value) {
if (key == 0) {
V oldValue = zeroValue;
zeroValue = value;
if (!hasZeroValue) {
hasZeroValue = true;
size++;
}
return oldValue;
}
int[] keyTable = this.keyTable;
// Check for existing keys.
int index1 = key & mask;
int key1 = keyTable[index1];
if (key1 == key) {
V oldValue = valueTable[index1];
valueTable[index1] = value;
return oldValue;
}
int index2 = hash2(key);
int key2 = keyTable[index2];
if (key2 == key) {
V oldValue = valueTable[index2];
valueTable[index2] = value;
return oldValue;
}
int index3 = hash3(key);
int key3 = keyTable[index3];
if (key3 == key) {
V oldValue = valueTable[index3];
valueTable[index3] = value;
return oldValue;
}
// Update key in the stash.
for (int i = capacity, n = i + stashSize; i < n; i++) {
if (keyTable[i] == key) {
V oldValue = valueTable[i];
valueTable[i] = value;
return oldValue;
}
}
// Check for empty buckets.
if (key1 == EMPTY) {
keyTable[index1] = key;
valueTable[index1] = value;
if (size++ >= threshold) resize(capacity << 1);
return null;
}
if (key2 == EMPTY) {
keyTable[index2] = key;
valueTable[index2] = value;
if (size++ >= threshold) resize(capacity << 1);
return null;
}
if (key3 == EMPTY) {
keyTable[index3] = key;
valueTable[index3] = value;
if (size++ >= threshold) resize(capacity << 1);
return null;
}
push(key, value, index1, key1, index2, key2, index3, key3);
return null;
}
public void putAll (IntMap<? extends V> map) {
for (Entry<? extends V> entry : map.entries())
put(entry.key, entry.value);
}
/** Skips checks for existing keys. */
private void putResize (int key, V value) {
if (key == 0) {
zeroValue = value;
hasZeroValue = true;
return;
}
// Check for empty buckets.
int index1 = key & mask;
int key1 = keyTable[index1];
if (key1 == EMPTY) {
keyTable[index1] = key;
valueTable[index1] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
int index2 = hash2(key);
int key2 = keyTable[index2];
if (key2 == EMPTY) {
keyTable[index2] = key;
valueTable[index2] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
int index3 = hash3(key);
int key3 = keyTable[index3];
if (key3 == EMPTY) {
keyTable[index3] = key;
valueTable[index3] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
push(key, value, index1, key1, index2, key2, index3, key3);
}
private void push (int insertKey, V insertValue, int index1, int key1, int index2, int key2, int index3, int key3) {
int[] keyTable = this.keyTable;
V[] valueTable = this.valueTable;
int mask = this.mask;
// Push keys until an empty bucket is found.
int evictedKey;
V evictedValue;
int i = 0, pushIterations = this.pushIterations;
do {
// Replace the key and value for one of the hashes.
switch (MathUtil.random.nextInt(2)) {
case 0:
evictedKey = key1;
evictedValue = valueTable[index1];
keyTable[index1] = insertKey;
valueTable[index1] = insertValue;
break;
case 1:
evictedKey = key2;
evictedValue = valueTable[index2];
keyTable[index2] = insertKey;
valueTable[index2] = insertValue;
break;
default:
evictedKey = key3;
evictedValue = valueTable[index3];
keyTable[index3] = insertKey;
valueTable[index3] = insertValue;
break;
}
// If the evicted key hashes to an empty bucket, put it there and stop.
index1 = evictedKey & mask;
key1 = keyTable[index1];
if (key1 == EMPTY) {
keyTable[index1] = evictedKey;
valueTable[index1] = evictedValue;
if (size++ >= threshold) resize(capacity << 1);
return;
}
index2 = hash2(evictedKey);
key2 = keyTable[index2];
if (key2 == EMPTY) {
keyTable[index2] = evictedKey;
valueTable[index2] = evictedValue;
if (size++ >= threshold) resize(capacity << 1);
return;
}
index3 = hash3(evictedKey);
key3 = keyTable[index3];
if (key3 == EMPTY) {
keyTable[index3] = evictedKey;
valueTable[index3] = evictedValue;
if (size++ >= threshold) resize(capacity << 1);
return;
}
if (++i == pushIterations) break;
insertKey = evictedKey;
insertValue = evictedValue;
} while (true);
putStash(evictedKey, evictedValue);
}
private void putStash (int key, V value) {
if (stashSize == stashCapacity) {
// Too many pushes occurred and the stash is full, increase the table size.
resize(capacity << 1);
putResize(key, value);
return;
}
// Store key in the stash.
int index = capacity + stashSize;
keyTable[index] = key;
valueTable[index] = value;
stashSize++;
size++;
}
public V get (int key) {
if (key == 0) {
if (!hasZeroValue) return null;
return zeroValue;
}
int index = key & mask;
if (keyTable[index] != key) {
index = hash2(key);
if (keyTable[index] != key) {
index = hash3(key);
if (keyTable[index] != key) return getStash(key, null);
}
}
return valueTable[index];
}
public V get (int key, V defaultValue) {
if (key == 0) {
if (!hasZeroValue) return defaultValue;
return zeroValue;
}
int index = key & mask;
if (keyTable[index] != key) {
index = hash2(key);
if (keyTable[index] != key) {
index = hash3(key);
if (keyTable[index] != key) return getStash(key, defaultValue);
}
}
return valueTable[index];
}
private V getStash (int key, V defaultValue) {
int[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++)
if (keyTable[i] == key) return valueTable[i];
return defaultValue;
}
public V remove (int key) {
if (key == 0) {
if (!hasZeroValue) return null;
V oldValue = zeroValue;
zeroValue = null;
hasZeroValue = false;
size--;
return oldValue;
}
int index = key & mask;
if (keyTable[index] == key) {
keyTable[index] = EMPTY;
V oldValue = valueTable[index];
valueTable[index] = null;
size--;
return oldValue;
}
index = hash2(key);
if (keyTable[index] == key) {
keyTable[index] = EMPTY;
V oldValue = valueTable[index];
valueTable[index] = null;
size--;
return oldValue;
}
index = hash3(key);
if (keyTable[index] == key) {
keyTable[index] = EMPTY;
V oldValue = valueTable[index];
valueTable[index] = null;
size--;
return oldValue;
}
return removeStash(key);
}
V removeStash (int key) {
int[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++) {
if (keyTable[i] == key) {
V oldValue = valueTable[i];
removeStashIndex(i);
size--;
return oldValue;
}
}
return null;
}
void removeStashIndex (int index) {
// If the removed location was not last, move the last tuple to the removed location.
stashSize--;
int lastIndex = capacity + stashSize;
if (index < lastIndex) {
keyTable[index] = keyTable[lastIndex];
valueTable[index] = valueTable[lastIndex];
valueTable[lastIndex] = null;
} else
valueTable[index] = null;
}
/** Returns true if the map is empty. */
public boolean isEmpty () {
return size == 0;
}
/** Reduces the size of the backing arrays to be the specified capacity or less. If the capacity is already less, nothing is
* done. If the map contains more items than the specified capacity, the next highest power of two capacity is used instead. */
public void shrink (int maximumCapacity) {
if (maximumCapacity < 0) throw new IllegalArgumentException("maximumCapacity must be >= 0: " + maximumCapacity);
if (size > maximumCapacity) maximumCapacity = size;
if (capacity <= maximumCapacity) return;
maximumCapacity = MathUtil.nextPowerOfTwo(maximumCapacity);
resize(maximumCapacity);
}
/** Clears the map and reduces the size of the backing arrays to be the specified capacity if they are larger. */
public void clear (int maximumCapacity) {
if (capacity <= maximumCapacity) {
clear();
return;
}
zeroValue = null;
hasZeroValue = false;
size = 0;
resize(maximumCapacity);
}
public void clear () {
if (size == 0) return;
int[] keyTable = this.keyTable;
V[] valueTable = this.valueTable;
for (int i = capacity + stashSize; i-- > 0;) {
keyTable[i] = EMPTY;
valueTable[i] = null;
}
size = 0;
stashSize = 0;
zeroValue = null;
hasZeroValue = false;
}
/** Returns true if the specified value is in the map. Note this traverses the entire map and compares every value, which may
* be an expensive operation.
* @param identity If true, uses == to compare the specified value with values in the map. If false, uses
* {@link #equals(Object)}. */
public boolean containsValue (Object value, boolean identity) {
V[] valueTable = this.valueTable;
if (value == null) {
if (hasZeroValue && zeroValue == null) return true;
int[] keyTable = this.keyTable;
for (int i = capacity + stashSize; i-- > 0;)
if (keyTable[i] != EMPTY && valueTable[i] == null) return true;
} else if (identity) {
if (value == zeroValue) return true;
for (int i = capacity + stashSize; i-- > 0;)
if (valueTable[i] == value) return true;
} else {
if (hasZeroValue && value.equals(zeroValue)) return true;
for (int i = capacity + stashSize; i-- > 0;)
if (value.equals(valueTable[i])) return true;
}
return false;
}
public boolean containsKey (int key) {
if (key == 0) return hasZeroValue;
int index = key & mask;
if (keyTable[index] != key) {
index = hash2(key);
if (keyTable[index] != key) {
index = hash3(key);
if (keyTable[index] != key) return containsKeyStash(key);
}
}
return true;
}
private boolean containsKeyStash (int key) {
int[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++)
if (keyTable[i] == key) return true;
return false;
}
/** Returns the key for the specified value, or <tt>notFound</tt> if it is not in the map. Note this traverses the entire map
* and compares every value, which may be an expensive operation.
* @param identity If true, uses == to compare the specified value with values in the map. If false, uses
* {@link #equals(Object)}. */
public int findKey (Object value, boolean identity, int notFound) {
V[] valueTable = this.valueTable;
if (value == null) {
if (hasZeroValue && zeroValue == null) return 0;
int[] keyTable = this.keyTable;
for (int i = capacity + stashSize; i-- > 0;)
if (keyTable[i] != EMPTY && valueTable[i] == null) return keyTable[i];
} else if (identity) {
if (value == zeroValue) return 0;
for (int i = capacity + stashSize; i-- > 0;)
if (valueTable[i] == value) return keyTable[i];
} else {
if (hasZeroValue && value.equals(zeroValue)) return 0;
for (int i = capacity + stashSize; i-- > 0;)
if (value.equals(valueTable[i])) return keyTable[i];
}
return notFound;
}
/** Increases the size of the backing array to accommodate the specified number of additional items. Useful before adding many
* items to avoid multiple backing array resizes. */
public void ensureCapacity (int additionalCapacity) {
if (additionalCapacity < 0) throw new IllegalArgumentException("additionalCapacity must be >= 0: " + additionalCapacity);
int sizeNeeded = size + additionalCapacity;
if (sizeNeeded >= threshold) resize(MathUtil.nextPowerOfTwo((int)Math.ceil(sizeNeeded / loadFactor)));
}
private void resize (int newSize) {
int oldEndIndex = capacity + stashSize;
capacity = newSize;
threshold = (int)(newSize * loadFactor);
mask = newSize - 1;
hashShift = 31 - Integer.numberOfTrailingZeros(newSize);
stashCapacity = Math.max(3, (int)Math.ceil(Math.log(newSize)) * 2);
pushIterations = Math.max(Math.min(newSize, 8), (int)Math.sqrt(newSize) / 8);
int[] oldKeyTable = keyTable;
V[] oldValueTable = valueTable;
keyTable = new int[newSize + stashCapacity];
valueTable = (V[])new Object[newSize + stashCapacity];
int oldSize = size;
size = hasZeroValue ? 1 : 0;
stashSize = 0;
if (oldSize > 0) {
for (int i = 0; i < oldEndIndex; i++) {
int key = oldKeyTable[i];
if (key != EMPTY) putResize(key, oldValueTable[i]);
}
}
}
private int hash2 (int h) {
h *= PRIME2;
return (h ^ h >>> hashShift) & mask;
}
private int hash3 (int h) {
h *= PRIME3;
return (h ^ h >>> hashShift) & mask;
}
@Override
public int hashCode () {
int h = 0;
if (hasZeroValue && zeroValue != null) {
h += zeroValue.hashCode();
}
int[] keyTable = this.keyTable;
V[] valueTable = this.valueTable;
for (int i = 0, n = capacity + stashSize; i < n; i++) {
int key = keyTable[i];
if (key != EMPTY) {
h += key * 31;
V value = valueTable[i];
if (value != null) {
h += value.hashCode();
}
}
}
return h;
}
@Override
public boolean equals (Object obj) {
if (obj == this) return true;
if (!(obj instanceof IntMap)) return false;
IntMap<V> other = (IntMap)obj;
if (other.size != size) return false;
if (other.hasZeroValue != hasZeroValue) return false;
if (hasZeroValue) {
if (other.zeroValue == null) {
if (zeroValue != null) return false;
} else {
if (!other.zeroValue.equals(zeroValue)) return false;
}
}
int[] keyTable = this.keyTable;
V[] valueTable = this.valueTable;
for (int i = 0, n = capacity + stashSize; i < n; i++) {
int key = keyTable[i];
if (key != EMPTY) {
V value = valueTable[i];
if (value == null) {
if (!other.containsKey(key) || other.get(key) != null) return false;
} else {
if (!value.equals(other.get(key))) return false;
}
}
}
return true;
}
@Override
public String toString () {
if (size == 0) return "[]";
StringBuilder buffer = new StringBuilder(32);
buffer.append('[');
int[] keyTable = this.keyTable;
V[] valueTable = this.valueTable;
int i = keyTable.length;
if (hasZeroValue) {
buffer.append("0=");
buffer.append(zeroValue);
} else {
while (i-- > 0) {
int key = keyTable[i];
if (key == EMPTY) continue;
buffer.append(key);
buffer.append('=');
buffer.append(valueTable[i]);
break;
}
}
while (i-- > 0) {
int key = keyTable[i];
if (key == EMPTY) continue;
buffer.append(", ");
buffer.append(key);
buffer.append('=');
buffer.append(valueTable[i]);
}
buffer.append(']');
return buffer.toString();
}
@Override
public Iterator<Entry<V>> iterator () {
return entries();
}
/** Returns an iterator for the entries in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */
public Entries<V> entries () {
if (entries1 == null) {
entries1 = new Entries(this);
entries2 = new Entries(this);
}
if (!entries1.valid) {
entries1.reset();
entries1.valid = true;
entries2.valid = false;
return entries1;
}
entries2.reset();
entries2.valid = true;
entries1.valid = false;
return entries2;
}
/** Returns an iterator for the values in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */
public Values<V> values () {
if (values1 == null) {
values1 = new Values(this);
values2 = new Values(this);
}
if (!values1.valid) {
values1.reset();
values1.valid = true;
values2.valid = false;
return values1;
}
values2.reset();
values2.valid = true;
values1.valid = false;
return values2;
}
/** Returns an iterator for the keys in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */
public Keys keys () {
if (keys1 == null) {
keys1 = new Keys(this);
keys2 = new Keys(this);
}
if (!keys1.valid) {
keys1.reset();
keys1.valid = true;
keys2.valid = false;
return keys1;
}
keys2.reset();
keys2.valid = true;
keys1.valid = false;
return keys2;
}
static public class Entry<V> {
public int key;
public V value;
@Override
public String toString () {
return key + "=" + value;
}
}
static private class MapIterator<V> {
static final int INDEX_ILLEGAL = -2;
static final int INDEX_ZERO = -1;
public boolean hasNext;
final IntMap<V> map;
int nextIndex, currentIndex;
boolean valid = true;
public MapIterator (IntMap<V> map) {
this.map = map;
reset();
}
public void reset () {
currentIndex = INDEX_ILLEGAL;
nextIndex = INDEX_ZERO;
if (map.hasZeroValue)
hasNext = true;
else
findNextIndex();
}
void findNextIndex () {
hasNext = false;
int[] keyTable = map.keyTable;
for (int n = map.capacity + map.stashSize; ++nextIndex < n;) {
if (keyTable[nextIndex] != EMPTY) {
hasNext = true;
break;
}
}
}
public void remove () {
if (currentIndex == INDEX_ZERO && map.hasZeroValue) {
map.zeroValue = null;
map.hasZeroValue = false;
} else if (currentIndex < 0) {
throw new IllegalStateException("next must be called before remove.");
} else if (currentIndex >= map.capacity) {
map.removeStashIndex(currentIndex);
nextIndex = currentIndex - 1;
findNextIndex();
} else {
map.keyTable[currentIndex] = EMPTY;
map.valueTable[currentIndex] = null;
}
currentIndex = INDEX_ILLEGAL;
map.size--;
}
}
@SuppressWarnings("rawtypes")
static public class Entries<V> extends MapIterator<V> implements Iterable<Entry<V>>, Iterator<Entry<V>> {
private Entry<V> entry = new Entry();
public Entries (IntMap map) {
super(map);
}
/** Note the same entry instance is returned each time this method is called. */
@Override
public Entry<V> next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
int[] keyTable = map.keyTable;
if (nextIndex == INDEX_ZERO) {
entry.key = 0;
entry.value = map.zeroValue;
} else {
entry.key = keyTable[nextIndex];
entry.value = map.valueTable[nextIndex];
}
currentIndex = nextIndex;
findNextIndex();
return entry;
}
@Override
public boolean hasNext () {
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
return hasNext;
}
@Override
public Iterator<Entry<V>> iterator () {
return this;
}
@Override
public void remove () {
super.remove();
}
}
@SuppressWarnings("unchecked")
static public class Values<V> extends MapIterator<V> implements Iterable<V>, Iterator<V> {
public Values (IntMap<V> map) {
super(map);
}
@Override
public boolean hasNext () {
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
return hasNext;
}
@Override
public V next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
V value;
if (nextIndex == INDEX_ZERO)
value = map.zeroValue;
else
value = map.valueTable[nextIndex];
currentIndex = nextIndex;
findNextIndex();
return value;
}
@Override
public Iterator<V> iterator () {
return this;
}
/** Returns a new array containing the remaining values. */
public Array<V> toArray () {
Array array = new Array(true, map.size);
while (hasNext)
array.add(next());
return array;
}
@Override
public void remove () {
super.remove();
}
}
@SuppressWarnings("unchecked")
static public class Keys extends MapIterator {
@SuppressWarnings("rawtypes")
public Keys (IntMap map) {
super(map);
}
public int next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
int key = nextIndex == INDEX_ZERO ? 0 : map.keyTable[nextIndex];
currentIndex = nextIndex;
findNextIndex();
return key;
}
/** Returns a new array containing the remaining keys. */
public IntArray toArray () {
IntArray array = new IntArray(true, map.size);
while (hasNext)
array.add(next());
return array;
}
}
}

View File

@ -0,0 +1,571 @@
/*******************************************************************************
* Copyright 2011 LibGDX.
* Mario Zechner <badlogicgames@gmail.com>
* Nathan Sweet <nathan.sweet@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package dorkbox.collections;
import java.util.NoSuchElementException;
import java.util.Random;
/** An unordered set that uses int keys. This implementation uses cuckoo hashing using 3 hashes, random walking, and a small stash
* for problematic keys. No allocation is done except when growing the table size. <br>
* <br>
* This set performs very fast contains and remove (typically O(1), worst case O(log(n))). Add may be a bit slower, depending on
* hash collisions. Load factors greater than 0.91 greatly increase the chances the set will have to rehash to the next higher POT
* size.
* @author Nathan Sweet */
public class IntSet {
private static final int PRIME1 = 0xbe1f14b1;
private static final int PRIME2 = 0xb4b82e39;
private static final int PRIME3 = 0xced1c241;
private static final int EMPTY = 0;
public int size;
int[] keyTable;
int capacity, stashSize;
boolean hasZeroValue;
private float loadFactor;
private int hashShift, mask, threshold;
private int stashCapacity;
private int pushIterations;
private IntSetIterator iterator1, iterator2;
/** Creates a new set with an initial capacity of 51 and a load factor of 0.8. */
public IntSet () {
this(51, 0.8f);
}
/** Creates a new set with a load factor of 0.8.
* @param initialCapacity If not a power of two, it is increased to the next nearest power of two. */
public IntSet (int initialCapacity) {
this(initialCapacity, 0.8f);
}
/** Creates a new set with the specified initial capacity and load factor. This set will hold initialCapacity items before
* growing the backing table.
* @param initialCapacity If not a power of two, it is increased to the next nearest power of two. */
public IntSet (int initialCapacity, float loadFactor) {
if (initialCapacity < 0) throw new IllegalArgumentException("initialCapacity must be >= 0: " + initialCapacity);
initialCapacity = MathUtil.nextPowerOfTwo((int)Math.ceil(initialCapacity / loadFactor));
if (initialCapacity > 1 << 30) throw new IllegalArgumentException("initialCapacity is too large: " + initialCapacity);
capacity = initialCapacity;
if (loadFactor <= 0) throw new IllegalArgumentException("loadFactor must be > 0: " + loadFactor);
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
mask = capacity - 1;
hashShift = 31 - Integer.numberOfTrailingZeros(capacity);
stashCapacity = Math.max(3, (int)Math.ceil(Math.log(capacity)) * 2);
pushIterations = Math.max(Math.min(capacity, 8), (int)Math.sqrt(capacity) / 8);
keyTable = new int[capacity + stashCapacity];
}
/** Creates a new set identical to the specified set. */
public IntSet (IntSet set) {
this((int)Math.floor(set.capacity * set.loadFactor), set.loadFactor);
stashSize = set.stashSize;
System.arraycopy(set.keyTable, 0, keyTable, 0, set.keyTable.length);
size = set.size;
hasZeroValue = set.hasZeroValue;
}
/** Returns true if the key was not already in the set. */
public boolean add (int key) {
if (key == 0) {
if (hasZeroValue) return false;
hasZeroValue = true;
size++;
return true;
}
int[] keyTable = this.keyTable;
// Check for existing keys.
int index1 = key & mask;
int key1 = keyTable[index1];
if (key1 == key) return false;
int index2 = hash2(key);
int key2 = keyTable[index2];
if (key2 == key) return false;
int index3 = hash3(key);
int key3 = keyTable[index3];
if (key3 == key) return false;
// Find key in the stash.
for (int i = capacity, n = i + stashSize; i < n; i++)
if (keyTable[i] == key) return false;
// Check for empty buckets.
if (key1 == EMPTY) {
keyTable[index1] = key;
if (size++ >= threshold) resize(capacity << 1);
return true;
}
if (key2 == EMPTY) {
keyTable[index2] = key;
if (size++ >= threshold) resize(capacity << 1);
return true;
}
if (key3 == EMPTY) {
keyTable[index3] = key;
if (size++ >= threshold) resize(capacity << 1);
return true;
}
push(key, index1, key1, index2, key2, index3, key3);
return true;
}
public void addAll (IntArray array) {
addAll(array.items, 0, array.size);
}
public void addAll (IntArray array, int offset, int length) {
if (offset + length > array.size)
throw new IllegalArgumentException("offset + length must be <= size: " + offset + " + " + length + " <= " + array.size);
addAll(array.items, offset, length);
}
public void addAll (int... array) {
addAll(array, 0, array.length);
}
public void addAll (int[] array, int offset, int length) {
ensureCapacity(length);
for (int i = offset, n = i + length; i < n; i++)
add(array[i]);
}
public void addAll (IntSet set) {
ensureCapacity(set.size);
IntSetIterator iterator = set.iterator();
while (iterator.hasNext)
add(iterator.next());
}
/** Skips checks for existing keys. */
private void addResize (int key) {
if (key == 0) {
hasZeroValue = true;
return;
}
// Check for empty buckets.
int index1 = key & mask;
int key1 = keyTable[index1];
if (key1 == EMPTY) {
keyTable[index1] = key;
if (size++ >= threshold) resize(capacity << 1);
return;
}
int index2 = hash2(key);
int key2 = keyTable[index2];
if (key2 == EMPTY) {
keyTable[index2] = key;
if (size++ >= threshold) resize(capacity << 1);
return;
}
int index3 = hash3(key);
int key3 = keyTable[index3];
if (key3 == EMPTY) {
keyTable[index3] = key;
if (size++ >= threshold) resize(capacity << 1);
return;
}
push(key, index1, key1, index2, key2, index3, key3);
}
private void push (int insertKey, int index1, int key1, int index2, int key2, int index3, int key3) {
int[] keyTable = this.keyTable;
int mask = this.mask;
// Push keys until an empty bucket is found.
int evictedKey;
int i = 0, pushIterations = this.pushIterations;
do {
// Replace the key and value for one of the hashes.
switch (MathUtil.random.nextInt(2)) {
case 0:
evictedKey = key1;
keyTable[index1] = insertKey;
break;
case 1:
evictedKey = key2;
keyTable[index2] = insertKey;
break;
default:
evictedKey = key3;
keyTable[index3] = insertKey;
break;
}
// If the evicted key hashes to an empty bucket, put it there and stop.
index1 = evictedKey & mask;
key1 = keyTable[index1];
if (key1 == EMPTY) {
keyTable[index1] = evictedKey;
if (size++ >= threshold) resize(capacity << 1);
return;
}
index2 = hash2(evictedKey);
key2 = keyTable[index2];
if (key2 == EMPTY) {
keyTable[index2] = evictedKey;
if (size++ >= threshold) resize(capacity << 1);
return;
}
index3 = hash3(evictedKey);
key3 = keyTable[index3];
if (key3 == EMPTY) {
keyTable[index3] = evictedKey;
if (size++ >= threshold) resize(capacity << 1);
return;
}
if (++i == pushIterations) break;
insertKey = evictedKey;
} while (true);
addStash(evictedKey);
}
private void addStash (int key) {
if (stashSize == stashCapacity) {
// Too many pushes occurred and the stash is full, increase the table size.
resize(capacity << 1);
addResize(key);
return;
}
// Store key in the stash.
int index = capacity + stashSize;
keyTable[index] = key;
stashSize++;
size++;
}
/** Returns true if the key was removed. */
public boolean remove (int key) {
if (key == 0) {
if (!hasZeroValue) return false;
hasZeroValue = false;
size--;
return true;
}
int index = key & mask;
if (keyTable[index] == key) {
keyTable[index] = EMPTY;
size--;
return true;
}
index = hash2(key);
if (keyTable[index] == key) {
keyTable[index] = EMPTY;
size--;
return true;
}
index = hash3(key);
if (keyTable[index] == key) {
keyTable[index] = EMPTY;
size--;
return true;
}
return removeStash(key);
}
boolean removeStash (int key) {
int[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++) {
if (keyTable[i] == key) {
removeStashIndex(i);
size--;
return true;
}
}
return false;
}
void removeStashIndex (int index) {
// If the removed location was not last, move the last tuple to the removed location.
stashSize--;
int lastIndex = capacity + stashSize;
if (index < lastIndex) keyTable[index] = keyTable[lastIndex];
}
/** Returns true if the set is empty. */
public boolean isEmpty () {
return size == 0;
}
/** Reduces the size of the backing arrays to be the specified capacity or less. If the capacity is already less, nothing is
* done. If the set contains more items than the specified capacity, the next highest power of two capacity is used instead. */
public void shrink (int maximumCapacity) {
if (maximumCapacity < 0) throw new IllegalArgumentException("maximumCapacity must be >= 0: " + maximumCapacity);
if (size > maximumCapacity) maximumCapacity = size;
if (capacity <= maximumCapacity) return;
maximumCapacity = MathUtil.nextPowerOfTwo(maximumCapacity);
resize(maximumCapacity);
}
/** Clears the set and reduces the size of the backing arrays to be the specified capacity if they are larger. */
public void clear (int maximumCapacity) {
if (capacity <= maximumCapacity) {
clear();
return;
}
hasZeroValue = false;
size = 0;
resize(maximumCapacity);
}
public void clear () {
if (size == 0) return;
int[] keyTable = this.keyTable;
for (int i = capacity + stashSize; i-- > 0;)
keyTable[i] = EMPTY;
size = 0;
stashSize = 0;
hasZeroValue = false;
}
public boolean contains (int key) {
if (key == 0) return hasZeroValue;
int index = key & mask;
if (keyTable[index] != key) {
index = hash2(key);
if (keyTable[index] != key) {
index = hash3(key);
if (keyTable[index] != key) return containsKeyStash(key);
}
}
return true;
}
private boolean containsKeyStash (int key) {
int[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++)
if (keyTable[i] == key) return true;
return false;
}
public int first () {
if (hasZeroValue) return 0;
int[] keyTable = this.keyTable;
for (int i = 0, n = capacity + stashSize; i < n; i++)
if (keyTable[i] != EMPTY) return keyTable[i];
throw new IllegalStateException("IntSet is empty.");
}
/** Increases the size of the backing array to accommodate the specified number of additional items. Useful before adding many
* items to avoid multiple backing array resizes. */
public void ensureCapacity (int additionalCapacity) {
if (additionalCapacity < 0) throw new IllegalArgumentException("additionalCapacity must be >= 0: " + additionalCapacity);
int sizeNeeded = size + additionalCapacity;
if (sizeNeeded >= threshold) resize(MathUtil.nextPowerOfTwo((int)Math.ceil(sizeNeeded / loadFactor)));
}
private void resize (int newSize) {
int oldEndIndex = capacity + stashSize;
capacity = newSize;
threshold = (int)(newSize * loadFactor);
mask = newSize - 1;
hashShift = 31 - Integer.numberOfTrailingZeros(newSize);
stashCapacity = Math.max(3, (int)Math.ceil(Math.log(newSize)) * 2);
pushIterations = Math.max(Math.min(newSize, 8), (int)Math.sqrt(newSize) / 8);
int[] oldKeyTable = keyTable;
keyTable = new int[newSize + stashCapacity];
int oldSize = size;
size = hasZeroValue ? 1 : 0;
stashSize = 0;
if (oldSize > 0) {
for (int i = 0; i < oldEndIndex; i++) {
int key = oldKeyTable[i];
if (key != EMPTY) addResize(key);
}
}
}
private int hash2 (int h) {
h *= PRIME2;
return (h ^ h >>> hashShift) & mask;
}
private int hash3 (int h) {
h *= PRIME3;
return (h ^ h >>> hashShift) & mask;
}
public int hashCode () {
int h = 0;
for (int i = 0, n = capacity + stashSize; i < n; i++)
if (keyTable[i] != EMPTY) h += keyTable[i];
return h;
}
public boolean equals (Object obj) {
if (!(obj instanceof IntSet)) return false;
IntSet other = (IntSet)obj;
if (other.size != size) return false;
if (other.hasZeroValue != hasZeroValue) return false;
for (int i = 0, n = capacity + stashSize; i < n; i++)
if (keyTable[i] != EMPTY && !other.contains(keyTable[i])) return false;
return true;
}
public String toString () {
if (size == 0) return "[]";
StringBuilder buffer = new StringBuilder(32);
buffer.append('[');
int[] keyTable = this.keyTable;
int i = keyTable.length;
if (hasZeroValue)
buffer.append("0");
else {
while (i-- > 0) {
int key = keyTable[i];
if (key == EMPTY) continue;
buffer.append(key);
break;
}
}
while (i-- > 0) {
int key = keyTable[i];
if (key == EMPTY) continue;
buffer.append(", ");
buffer.append(key);
}
buffer.append(']');
return buffer.toString();
}
/** Returns an iterator for the keys in the set. Remove is supported. Note that the same iterator instance is returned each time
* this method is called. Use the {@link IntSetIterator} constructor for nested or multithreaded iteration. */
public IntSetIterator iterator () {
if (iterator1 == null) {
iterator1 = new IntSetIterator(this);
iterator2 = new IntSetIterator(this);
}
if (!iterator1.valid) {
iterator1.reset();
iterator1.valid = true;
iterator2.valid = false;
return iterator1;
}
iterator2.reset();
iterator2.valid = true;
iterator1.valid = false;
return iterator2;
}
static public IntSet with (int... array) {
IntSet set = new IntSet();
set.addAll(array);
return set;
}
static public class IntSetIterator {
static final int INDEX_ILLEGAL = -2;
static final int INDEX_ZERO = -1;
public boolean hasNext;
final IntSet set;
int nextIndex, currentIndex;
boolean valid = true;
public IntSetIterator (IntSet set) {
this.set = set;
reset();
}
public void reset () {
currentIndex = INDEX_ILLEGAL;
nextIndex = INDEX_ZERO;
if (set.hasZeroValue)
hasNext = true;
else
findNextIndex();
}
void findNextIndex () {
hasNext = false;
int[] keyTable = set.keyTable;
for (int n = set.capacity + set.stashSize; ++nextIndex < n;) {
if (keyTable[nextIndex] != EMPTY) {
hasNext = true;
break;
}
}
}
public void remove () {
if (currentIndex == INDEX_ZERO && set.hasZeroValue) {
set.hasZeroValue = false;
} else if (currentIndex < 0) {
throw new IllegalStateException("next must be called before remove.");
} else if (currentIndex >= set.capacity) {
set.removeStashIndex(currentIndex);
nextIndex = currentIndex - 1;
findNextIndex();
} else {
set.keyTable[currentIndex] = EMPTY;
}
currentIndex = INDEX_ILLEGAL;
set.size--;
}
public int next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
int key = nextIndex == INDEX_ZERO ? 0 : set.keyTable[nextIndex];
currentIndex = nextIndex;
findNextIndex();
return key;
}
/** Returns a new array containing the remaining keys. */
public IntArray toArray () {
IntArray array = new IntArray(true, set.size);
while (hasNext)
array.add(next());
return array;
}
}
}

View File

@ -0,0 +1,243 @@
/*
* Copyright 2018 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.collections;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.RandomAccess;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
/**
* This class uses the "single-writer-principle" for lock-free publication.
*
* Since there are only 2 methods to guarantee that modifications can only be called one-at-a-time (either it is only called by
* one thread, or only one thread can access it at a time) -- we chose the 2nd option -- and use 'synchronized' to make sure that only
* one thread can access this modification methods at a time. Getting or checking the presence of values can then happen in a lock-free
* manner.
*
* According to my benchmarks, this is approximately 25% faster than ConcurrentHashMap for (all types of) reads, and a lot slower for
* contended writes.
*
* This data structure is for many-read/few-write scenarios
*/
public final
class LockFreeArrayList<E> implements List<E>, RandomAccess, Cloneable, Serializable {
// Recommended for best performance while adhering to the "single writer principle". Must be static-final
private static final AtomicReferenceFieldUpdater<LockFreeArrayList, ArrayList> listRef =
AtomicReferenceFieldUpdater.newUpdater(LockFreeArrayList.class,
ArrayList.class, "arrayList");
private volatile ArrayList<E> arrayList = new ArrayList<>();
public
LockFreeArrayList(){}
public
LockFreeArrayList(Collection<E> elements) {
arrayList.addAll(elements);
}
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
// use-case 99% of the time)
public synchronized
void clear() {
arrayList.clear();
}
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
// use-case 99% of the time)
public synchronized
boolean add(final E element) {
return arrayList.add(element);
}
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
// use-case 99% of the time)
public synchronized
boolean addAll(final Collection<? extends E> elements) {
return arrayList.addAll(elements);
}
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
// use-case 99% of the time)
@Override
public synchronized
boolean addAll(final int i, final Collection<? extends E> collection) {
return arrayList.addAll(i, collection);
}
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
// use-case 99% of the time)
@Override
public
boolean removeAll(final Collection<?> collection) {
return arrayList.removeAll(collection);
}
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
// use-case 99% of the time)
@Override
public synchronized
boolean retainAll(final Collection<?> collection) {
return retainAll(collection);
}
public
E get(int index) {
//noinspection unchecked
return (E) listRef.get(this).get(index);
}
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
// use-case 99% of the time)
@Override
public synchronized
E set(final int index, final E element) {
return arrayList.set(index, element);
}
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
// use-case 99% of the time)
@Override
public synchronized
void add(final int index, final E element) {
arrayList.add(index, element);
}
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
// use-case 99% of the time)
@Override
public synchronized
E remove(final int index) {
return arrayList.remove(index);
}
// lock-free get
@Override
public
int indexOf(final Object object) {
return listRef.get(this).indexOf(object);
}
// lock-free get
@Override
public
int lastIndexOf(final Object object) {
return listRef.get(this).lastIndexOf(object);
}
// lock-free get
@Override
public
ListIterator<E> listIterator() {
return listRef.get(this).listIterator();
}
// lock-free get
@Override
public
ListIterator<E> listIterator(final int index) {
return listRef.get(this).listIterator(index);
}
// lock-free get
@Override
public
List<E> subList(final int startIndex, final int endIndex) {
return listRef.get(this).subList(startIndex, endIndex);
}
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
// use-case 99% of the time)
public synchronized
boolean remove(final Object element) {
return arrayList.remove(element);
}
// lock-free get
@Override
public
boolean containsAll(final Collection<?> collection) {
//noinspection unchecked
return listRef.get(this).containsAll(collection);
}
// lock-free get
public
int size() {
return listRef.get(this).size();
}
// lock-free get
@Override
public
boolean isEmpty() {
return listRef.get(this).isEmpty();
}
// lock-free get
public
boolean contains(final Object element) {
// use the SWP to get the value
return listRef.get(this).contains(element);
}
// lock-free get
@Override
public
Iterator<E> iterator() {
return listRef.get(this).iterator();
}
// lock-free get
@Override
public
Object[] toArray() {
return listRef.get(this).toArray();
}
// lock-free get
@Override
public
<T> T[] toArray(final T[] targetArray) {
//noinspection unchecked
return (T[]) listRef.get(this).toArray(targetArray);
}
// lock-free get
@SuppressWarnings("unchecked")
public
ArrayList<E> elements() {
return listRef.get(this);
}
}

View File

@ -0,0 +1,407 @@
/*
* Copyright 2018 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.collections;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
/**
* A bimap (or "bidirectional map") is a map that preserves the uniqueness of its values as well as that of its keys. This constraint
* enables bimaps to support an "inverse view", which is another bimap containing the same entries as this bimap but with reversed keys and values.
*
* This class uses the "single-writer-principle" for lock-free publication.
*
* Since there are only 2 methods to guarantee that modifications can only be called one-at-a-time (either it is only called by
* one thread, or only one thread can access it at a time) -- we chose the 2nd option -- and use 'synchronized' to make sure that only
* one thread can access this modification methods at a time. Getting or checking the presence of values can then happen in a lock-free
* manner.
*
* According to my benchmarks, this is approximately 25% faster than ConcurrentHashMap for (all types of) reads, and a lot slower for
* contended writes.
*
* This data structure is for many-read/few-write scenarios
*/
@SuppressWarnings("WeakerAccess")
public final
class LockFreeBiMap<K, V> {
// Recommended for best performance while adhering to the "single writer principle". Must be static-final
private static final AtomicReferenceFieldUpdater<LockFreeBiMap, HashMap> forwardREF =
AtomicReferenceFieldUpdater.newUpdater(LockFreeBiMap.class,
HashMap.class,
"forwardHashMap");
private static final AtomicReferenceFieldUpdater<LockFreeBiMap, HashMap> reverseREF =
AtomicReferenceFieldUpdater.newUpdater(LockFreeBiMap.class,
HashMap.class,
"reverseHashMap");
private volatile HashMap<K, V> forwardHashMap;
private volatile HashMap<V, K> reverseHashMap;
private final LockFreeBiMap<V, K> inverse;
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
// use-case 99% of the time)
public
LockFreeBiMap() {
forwardHashMap = new HashMap<K, V>();
reverseHashMap = new HashMap<V, K>();
this.inverse = new LockFreeBiMap<V, K>(reverseHashMap, forwardHashMap, this);
}
private
LockFreeBiMap(final HashMap<K, V> forwardHashMap, final HashMap<V, K> reverseHashMap, final LockFreeBiMap<V, K> inverse) {
this.forwardHashMap = forwardHashMap;
this.reverseHashMap = reverseHashMap;
this.inverse = inverse;
}
/**
* Removes all of the mappings from this bimap.
* The bimap will be empty after this call returns.
*/
public synchronized
void clear() {
forwardHashMap.clear();
reverseHashMap.clear();
}
/**
* @return the inverse view of this bimap, which maps each of this bimap's values to its associated key.
*/
public
LockFreeBiMap<V, K> inverse() {
return inverse;
}
/**
* Replaces all of the mappings from the specified map to this bimap.
* These mappings will replace any mappings that this map had for
* any of the keys currently in the specified map.
*
* @param hashMap mappings to be stored in this map
*
* @throws NullPointerException if the specified map is null
*
* @throws IllegalArgumentException if a given value in the map is already bound to a different key in this bimap. The bimap will remain
* unmodified in this event. To avoid this exception, call {@link #replaceAllForce(Map)} replaceAllForce(map) instead.
*/
public synchronized
void replaceAll(final Map<K, V> hashMap) throws IllegalArgumentException {
if (hashMap == null) {
throw new NullPointerException("hashMap");
}
LockFreeBiMap<K, V> biMap = new LockFreeBiMap<K, V>();
try {
biMap.putAll(hashMap);
} catch (IllegalArgumentException e) {
// do nothing if there is an exception
throw e;
}
// only if there are no problems with the creation of the new bimap.
this.forwardHashMap.clear();
this.reverseHashMap.clear();
this.forwardHashMap.putAll(biMap.forwardHashMap);
this.reverseHashMap.putAll(biMap.reverseHashMap);
}
/**
* Replaces all of the mappings from the specified map to this bimap.
* These mappings will replace any mappings that this map had for
* any of the keys currently in the specified map. This is an alternate
* form of {@link #replaceAll(Map)} replaceAll(K, V) that will silently
* ignore duplicates
*
* @param hashMap mappings to be stored in this map
*
* @throws NullPointerException if the specified map is null
*/
public synchronized
void replaceAllForce(final Map<K, V> hashMap) {
if (hashMap == null) {
throw new NullPointerException("hashMap");
}
// only if there are no problems with the creation of the new bimap.
this.forwardHashMap.clear();
this.reverseHashMap.clear();
putAllForce(hashMap);
}
/**
* Associates the specified value with the specified key in this bimap.
* If the bimap previously contained a mapping for the key, the old
* value is replaced. If the given value is already bound to a different
* key in this bimap, the bimap will remain unmodified. To avoid throwing
* an exception, call {@link #putForce(Object, Object)} putForce(K, V) instead.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
*
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*
* @throws IllegalArgumentException if the given value is already bound to a different key in this bimap. The bimap will remain
* unmodified in this event. To avoid this exception, call {@link #putForce(Object, Object)} putForce(K, V) instead.
*/
public synchronized
V put(final K key, final V value) throws IllegalArgumentException {
V prevForwardValue = this.forwardHashMap.put(key, value);
if (prevForwardValue != null) {
reverseHashMap.remove(prevForwardValue);
}
K prevReverseValue = this.reverseHashMap.put(value, key);
if (prevReverseValue != null) {
// put the old value back
if (prevForwardValue != null) {
this.forwardHashMap.put(key, prevForwardValue);
}
else {
this.forwardHashMap.remove(key);
}
this.reverseHashMap.put(value, prevReverseValue);
throw new IllegalArgumentException("Value already exists. Keys and values must both be unique!");
}
return prevForwardValue;
}
/**
* Associates the specified value with the specified key in this bimap.
* If the bimap previously contained a mapping for the key, the old
* value is replaced. This is an alternate form of {@link #put(Object, Object)} put(K, V)
* that will silently ignore duplicates
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
*
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public synchronized
V putForce(final K key, final V value) {
V prevForwardValue = this.forwardHashMap.put(key, value);
if (prevForwardValue != null) {
reverseHashMap.remove(prevForwardValue);
}
K prevReverseValue = this.reverseHashMap.put(value, key);
if (prevReverseValue != null) {
forwardHashMap.remove(prevReverseValue);
}
return prevForwardValue;
}
/**
* Copies all of the mappings from the specified map to this map.
* These mappings will replace any mappings that this map had for
* any of the keys currently in the specified map.
*
* @param hashMap mappings to be stored in this map
*
* @throws NullPointerException if the specified map is null
*
* @throws IllegalArgumentException if the given value is already bound to a different key in this bimap. The bimap will remain
* unmodified in this event. To avoid this exception, call {@link #putAllForce(Map)} putAllForce(K, V) instead.
*/
public synchronized
void putAll(final Map<K, V> hashMap) throws IllegalArgumentException {
LockFreeBiMap<K, V> biMap = new LockFreeBiMap<K, V>();
try {
for (Map.Entry<K, V> entry : hashMap.entrySet()) {
K key = entry.getKey();
V value = entry.getValue();
biMap.put(key, value);
// we have to verify that the keys/values between the bimaps are unique
if (this.forwardHashMap.containsKey(key)) {
throw new IllegalArgumentException("Key already exists. Keys and values must both be unique!");
}
if (this.reverseHashMap.containsKey(value)) {
throw new IllegalArgumentException("Value already exists. Keys and values must both be unique!");
}
}
} catch (IllegalArgumentException e) {
// do nothing if there is an exception
throw e;
}
// only if there are no problems with the creation of the new bimap AND the uniqueness constrain is guaranteed
this.forwardHashMap.putAll(biMap.forwardHashMap);
this.reverseHashMap.putAll(biMap.reverseHashMap);
}
/**
* Copies all of the mappings from the specified map to this map.
* These mappings will replace any mappings that this map had for
* any of the keys currently in the specified map. This is an alternate
* form of {@link #putAll(Map)} putAll(K, V) that will silently
* ignore duplicates
*
* @param hashMap mappings to be stored in this map
*
* @throws NullPointerException if the specified map is null
*/
public synchronized
void putAllForce(final Map<K, V> hashMap) {
for (Map.Entry<K, V> entry : hashMap.entrySet()) {
K key = entry.getKey();
V value = entry.getValue();
putForce(key, value);
}
}
/**
* Removes the mapping for the specified key from this map if present.
*
* @param key key whose mapping is to be removed from the map
*
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public synchronized
V remove(final K key) {
V value = forwardHashMap.remove(key);
reverseHashMap.remove(value);
return value;
}
/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
* <p>
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
* key.equals(k))}, then this method returns {@code v}; otherwise
* it returns {@code null}. (There can be at most one such mapping.)
* <p>
* <p>A return value of {@code null} does not <i>necessarily</i>
* indicate that the map contains no mapping for the key; it's also
* possible that the map explicitly maps the key to {@code null}.
* The {@link HashMap#containsKey containsKey} operation may be used to
* distinguish these two cases.
*
* @see #put(Object, Object)
*/
@SuppressWarnings("unchecked")
public
V get(final K key) {
// use the SWP to get a lock-free get of the value
return (V) forwardREF.get(this).get(key);
}
/**
* Returns the reverse key to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
* <p>
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
* key.equals(k))}, then this method returns {@code v}; otherwise
* it returns {@code null}. (There can be at most one such mapping.)
* <p>
* <p>A return value of {@code null} does not <i>necessarily</i>
* indicate that the map contains no mapping for the key; it's also
* possible that the map explicitly maps the key to {@code null}.
* The {@link HashMap#containsKey containsKey} operation may be used to
* distinguish these two cases.
*
* @see #put(Object, Object)
*/
@SuppressWarnings("unchecked")
public
K getReverse(final V key) {
// use the SWP to get a lock-free get of the value
return (K) reverseREF.get(this).get(key);
}
/**
* Returns a {@link Collection} view of the values contained in this map.
* The collection is backed by the map, so changes to the map are
* reflected in the collection, and vice-versa. If the map is
* modified while an iteration over the collection is in progress
* (except through the iterator's own <tt>remove</tt> operation),
* the results of the iteration are undefined. The collection
* supports element removal, which removes the corresponding
* mapping from the map, via the <tt>Iterator.remove</tt>,
* <tt>Collection.remove</tt>, <tt>removeAll</tt>,
* <tt>retainAll</tt> and <tt>clear</tt> operations. It does not
* support the <tt>add</tt> or <tt>addAll</tt> operations.
*
* @return a view of the values contained in this map
*/
@SuppressWarnings("unchecked")
public
Collection<V> values() {
// use the SWP to get a lock-free get of the value
return forwardREF.get(this).values();
}
/**
* Returns <tt>true</tt> if this bimap contains no key-value mappings.
*
* @return <tt>true</tt> if this bimap contains no key-value mappings
*/
public
boolean isEmpty() {
// use the SWP to get a lock-free get of the value
return forwardREF.get(this)
.isEmpty();
}
/**
* Returns a {@link Collection} view of the values contained in this map.
* The collection is backed by the map, so changes to the map are
* reflected in the collection, and vice-versa. If the map is
* modified while an iteration over the collection is in progress
* (except through the iterator's own <tt>remove</tt> operation),
* the results of the iteration are undefined. The collection
* supports element removal, which removes the corresponding
* mapping from the map, via the <tt>Iterator.remove</tt>,
* <tt>Collection.remove</tt>, <tt>removeAll</tt>,
* <tt>retainAll</tt> and <tt>clear</tt> operations. It does not
* support the <tt>add</tt> or <tt>addAll</tt> operations.
*
* @return a view of the values contained in this map
*/
@SuppressWarnings("unchecked")
public
Collection<K> reverseValues() {
// use the SWP to get a lock-free get of the value
return reverseREF.get(this).values();
}
}

View File

@ -0,0 +1,269 @@
/*
* Copyright 2015 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.collections;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
/**
* This class uses the "single-writer-principle" for lock-free publication.
* <p>
* Since there are only 2 methods to guarantee that modifications can only be called one-at-a-time (either it is only called by
* one thread, or only one thread can access it at a time) -- we chose the 2nd option -- and use 'synchronized' to make sure that only
* one thread can access this modification methods at a time. Getting or checking the presence of values can then happen in a lock-free
* manner.
* <p>
* According to my benchmarks, this is approximately 25% faster than ConcurrentHashMap for (all types of) reads, and a lot slower for
* contended writes.
* <p>
* This data structure is for many-read/few-write scenarios
*/
public final
class LockFreeHashMap<K, V> implements Map<K, V>, Cloneable, Serializable {
// Recommended for best performance while adhering to the "single writer principle". Must be static-final
private static final AtomicReferenceFieldUpdater<LockFreeHashMap, HashMap> mapREF = AtomicReferenceFieldUpdater.newUpdater(
LockFreeHashMap.class,
HashMap.class,
"hashMap");
private volatile HashMap<K, V> hashMap;
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
// use-case 99% of the time)
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public
LockFreeHashMap() {
hashMap = new HashMap<K, V>();
}
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
*
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public
LockFreeHashMap(int initialCapacity) {
hashMap = new HashMap<K, V>(initialCapacity);
}
/**
* Constructs a new <tt>HashMap</tt> with the same mappings as the
* specified <tt>Map</tt>. The <tt>HashMap</tt> is created with
* default load factor (0.75) and an initial capacity sufficient to
* hold the mappings in the specified <tt>Map</tt>.
*
* @param map the map whose mappings are to be placed in this map
*
* @throws NullPointerException if the specified map is null
*/
public
LockFreeHashMap(Map<K, V> map) {
this.hashMap = new HashMap<K, V>(map);
}
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
*
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public
LockFreeHashMap(int initialCapacity, float loadFactor) {
this.hashMap = new HashMap<K, V>(initialCapacity, loadFactor);
}
@SuppressWarnings("unchecked")
public
Map<K, V> getMap() {
// use the SWP to get a lock-free get of the map. It's values are only valid at the moment this method is called.
return Collections.unmodifiableMap(mapREF.get(this));
}
@Override
public
int size() {
// use the SWP to get a lock-free get of the value
return mapREF.get(this)
.size();
}
@Override
public
boolean isEmpty() {
// use the SWP to get a lock-free get of the value
return mapREF.get(this)
.isEmpty();
}
@Override
public
boolean containsKey(final Object key) {
// use the SWP to get a lock-free get of the value
return mapREF.get(this)
.containsKey(key);
}
@Override
public
boolean containsValue(final Object value) {
// use the SWP to get a lock-free get of the value
return mapREF.get(this)
.containsValue(value);
}
@SuppressWarnings("unchecked")
@Override
public
V get(final Object key) {
// use the SWP to get a lock-free get of the value
return (V) mapREF.get(this)
.get(key);
}
@Override
public synchronized
V put(final K key, final V value) {
return hashMap.put(key, value);
}
@Override
public synchronized
V remove(final Object key) {
return hashMap.remove(key);
}
@SuppressWarnings("Java8CollectionRemoveIf")
public synchronized
void removeAllValues(final V value) {
for (Iterator<Entry<K, V>> iterator = hashMap.entrySet().iterator(); iterator.hasNext(); ) {
final Map.Entry<K, V> kvEntry = iterator.next();
if (kvEntry.getValue().equals(value)) {
iterator.remove();
}
}
}
@Override
public synchronized
void putAll(final Map<? extends K, ? extends V> map) {
this.hashMap.putAll(map);
}
/**
* This uses equals to update values. At first glance, this seems like a waste (since if it's equal, why update it?). This is because
* the ONLY location this is used (in the Database, for updating all DeviceUser in the map), equals compares ONLY the DB ID. In only
* this situation, this makes sense (since anything with the same DB ID, we should replace/update the value)
*/
public synchronized
void updateAllWithValue(final V value) {
for (Map.Entry<K, V> entry : hashMap.entrySet()) {
if (value.equals(entry.getValue())) {
// get's all device IDs that have this user assigned, and reassign the value
entry.setValue(value);
}
}
}
public synchronized
void replaceAll(Map<K,V> hashMap) {
this.hashMap.clear();
this.hashMap.putAll(hashMap);
}
@Override
public synchronized
void clear() {
hashMap.clear();
}
@Override
public
Set<K> keySet() {
return getMap().keySet();
}
@Override
public
Collection<V> values() {
return getMap().values();
}
@Override
public
Set<Entry<K, V>> entrySet() {
return getMap().entrySet();
}
@Override
public
boolean equals(final Object o) {
return mapREF.get(this)
.equals(o);
}
@Override
public
int hashCode() {
return mapREF.get(this)
.hashCode();
}
@Override
public
String toString() {
return mapREF.get(this)
.toString();
}
@SuppressWarnings("unchecked")
public
Collection<K> keys() {
// use the SWP to get a lock-free get of the value
return mapREF.get(this).keySet();
}
@SuppressWarnings("unchecked")
public
Map<K,V> elements() {
// use the SWP to get a lock-free get of the value
return mapREF.get(this);
}
@SuppressWarnings("unchecked")
public
HashMap<K, V> backingMap() {
// use the SWP to get a lock-free get of the value
return mapREF.get(this);
}
}

View File

@ -0,0 +1,157 @@
/*
* Copyright 2018 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.collections;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
/**
* This class uses the "single-writer-principle" for lock-free publication.
*
* Since there are only 2 methods to guarantee that modifications can only be called one-at-a-time (either it is only called by
* one thread, or only one thread can access it at a time) -- we chose the 2nd option -- and use 'synchronized' to make sure that only
* one thread can access this modification methods at a time. Getting or checking the presence of values can then happen in a lock-free
* manner.
*
* According to my benchmarks, this is approximately 25% faster than ConcurrentHashMap for (all types of) reads, and a lot slower for
* contended writes.
*
* This data structure is for many-read/few-write scenarios
*/
public final
class LockFreeHashSet<E> implements Set<E>, Cloneable, Serializable {
// Recommended for best performance while adhering to the "single writer principle". Must be static-final
private static final AtomicReferenceFieldUpdater<LockFreeHashSet, Set> setREF =
AtomicReferenceFieldUpdater.newUpdater(LockFreeHashSet.class,
Set.class,
"hashSet");
private volatile Set<E> hashSet = new HashSet<>();
public
LockFreeHashSet(){}
public
LockFreeHashSet(Collection<E> elements) {
hashSet.addAll(elements);
}
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
// use-case 99% of the time)
public synchronized
void clear() {
hashSet.clear();
}
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
// use-case 99% of the time)
public synchronized
boolean add(final E element) {
return hashSet.add(element);
}
@Override
public
boolean containsAll(final Collection<?> collection) {
return setREF.get(this).containsAll(collection);
}
@Override
public synchronized
boolean retainAll(final Collection<?> collection) {
return hashSet.retainAll(collection);
}
@Override
public synchronized
boolean removeAll(final Collection<?> collection) {
return hashSet.removeAll(collection);
}
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
// use-case 99% of the time)
public synchronized
boolean addAll(final Collection<? extends E> collection) {
return hashSet.addAll(collection);
}
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
// use-case 99% of the time)
public synchronized
boolean remove(final Object element) {
return hashSet.remove(element);
}
// lock-free get
@Override
public
boolean contains(final Object element) {
return setREF.get(this).contains(element);
}
// lock-free get
@SuppressWarnings("unchecked")
public
Set<E> elements() {
return setREF.get(this);
}
// lock-free get
public
int size() {
return setREF.get(this).size();
}
// lock-free get
@Override
public
boolean isEmpty() {
return setREF.get(this).isEmpty();
}
// lock-free get
@Override
public
Iterator<E> iterator() {
//noinspection unchecked
return setREF.get(this).iterator();
}
// lock-free get
@Override
public
Object[] toArray() {
return setREF.get(this).toArray();
}
// lock-free get
@Override
public
<T> T[] toArray(final T[] targetArray) {
//noinspection unchecked
return (T[]) setREF.get(this).toArray(targetArray);
}
}

View File

@ -0,0 +1,438 @@
/*
* Copyright 2018 dorkbox, llc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.collections;
import static dorkbox.collections.IntMap.*;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
/**
* A bimap (or "bidirectional map") is a map that preserves the uniqueness of its values as well as that of its keys. This constraint
* enables bimaps to support an "inverse view", which is another bimap containing the same entries as this bimap but with reversed keys and values.
*
* This class uses the "single-writer-principle" for lock-free publication.
*
* Since there are only 2 methods to guarantee that modifications can only be called one-at-a-time (either it is only called by
* one thread, or only one thread can access it at a time) -- we chose the 2nd option -- and use 'synchronized' to make sure that only
* one thread can access this modification methods at a time. Getting or checking the presence of values can then happen in a lock-free
* manner.
*
* According to my benchmarks, this is approximately 25% faster than ConcurrentHashMap for (all types of) reads, and a lot slower for
* contended writes.
*
* This data structure is for many-read/few-write scenarios
*/
public
class LockFreeIntBiMap<V> {
// Recommended for best performance while adhering to the "single writer principle". Must be static-final
private static final AtomicReferenceFieldUpdater<LockFreeIntBiMap, IntMap> forwardREF = AtomicReferenceFieldUpdater.newUpdater(
LockFreeIntBiMap.class,
IntMap.class,
"forwardHashMap");
private static final AtomicReferenceFieldUpdater<LockFreeIntBiMap, ObjectIntMap> reverseREF = AtomicReferenceFieldUpdater.newUpdater(
LockFreeIntBiMap.class,
ObjectIntMap.class,
"reverseHashMap");
private volatile IntMap<V> forwardHashMap;
private volatile ObjectIntMap<V> reverseHashMap;
private final int defaultReturnValue;
private final LockFreeObjectIntBiMap<V> inverse;
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
// use-case 99% of the time)
// public static
// void main(String[] args) {
// LockFreeIntBiMap<String> test = new LockFreeIntBiMap<String>();
// String one = "One";
// String four = "Four";
//
// test.put(1, one);
// test.put(2, "Two");
// test.put(3, "Three");
// test.put(4, four);
// // try {
// // test.put(1, four);
// // } catch (IllegalArgumentException e) {
// // }
// test.putForce(1, four);
// test.put(5, one);
//
// System.out.println(test.toString());
//
// System.out.println("Reverse");
// System.out.println(test.inverse().toString());
// }
/**
* Creates a new bimap using @{link Integer#MIN_VALUE}.
*/
public
LockFreeIntBiMap() {
this(Integer.MIN_VALUE);
}
/**
* The default return value is used for various get/put operations on the IntMap/ObjectIntMap.
*
* @param defaultReturnValue value used for various get/put operations on the IntMap/ObjectIntMap.
*/
public
LockFreeIntBiMap(int defaultReturnValue) {
this(new IntMap<V>(), new ObjectIntMap<V>(), defaultReturnValue);
}
/**
* The default return value is used for various get/put operations on the IntMap/ObjectIntMap.
*
* @param defaultReturnValue value used for various get/put operations on the IntMap/ObjectIntMap.
*/
public
LockFreeIntBiMap(IntMap<V> forwardHashMap, ObjectIntMap<V> reverseHashMap, int defaultReturnValue) {
this.forwardHashMap = forwardHashMap;
this.reverseHashMap = reverseHashMap;
this.defaultReturnValue = defaultReturnValue;
this.inverse = new LockFreeObjectIntBiMap<V>(reverseHashMap, forwardHashMap, defaultReturnValue, this);
}
LockFreeIntBiMap(final IntMap<V> forwardHashMap,
final ObjectIntMap<V> reverseHashMap,
final int defaultReturnValue,
final LockFreeObjectIntBiMap<V> inverse) {
this.forwardHashMap = forwardHashMap;
this.reverseHashMap = reverseHashMap;
this.defaultReturnValue = defaultReturnValue;
this.inverse = inverse;
}
/**
* Removes all of the mappings from this bimap.
* The bimap will be empty after this call returns.
*/
public synchronized
void clear() {
forwardHashMap.clear();
reverseHashMap.clear();
}
/**
* @return the inverse view of this bimap, which maps each of this bimap's values to its associated key.
*/
public
LockFreeObjectIntBiMap<V> inverse() {
return inverse;
}
/**
* Associates the specified value with the specified key in this bimap.
* If the bimap previously contained a mapping for the key, the old
* value is replaced. If the given value is already bound to a different
* key in this bimap, the bimap will remain unmodified. To avoid throwing
* an exception, call {@link #putForce(int, Object)} putForce(K, V) instead.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
*
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*
* @throws IllegalArgumentException if the given value is already bound to a different key in this bimap. The bimap will remain
* unmodified in this event. To avoid this exception, call {@link #putForce(int, Object)} putForce(K, V) instead.
*/
public synchronized
V put(final int key, final V value) throws IllegalArgumentException {
V prevForwardValue = this.forwardHashMap.put(key, value);
if (prevForwardValue != null) {
reverseHashMap.remove(prevForwardValue, defaultReturnValue);
}
int prevReverseValue = this.reverseHashMap.get(value, defaultReturnValue);
this.reverseHashMap.put(value, key);
if (prevReverseValue != defaultReturnValue) {
// put the old value back
if (prevForwardValue != null) {
this.forwardHashMap.put(key, prevForwardValue);
}
else {
this.forwardHashMap.remove(key);
}
this.reverseHashMap.put(value, prevReverseValue);
throw new IllegalArgumentException("Value already exists. Keys and values must both be unique!");
}
return prevForwardValue;
}
/**
* Associates the specified value with the specified key in this bimap.
* If the bimap previously contained a mapping for the key, the old
* value is replaced. This is an alternate form of {@link #put(int, Object)}
* that will silently ignore duplicates
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
*
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public synchronized
V putForce(final int key, final V value) {
V prevForwardValue = this.forwardHashMap.put(key, value);
if (prevForwardValue != null) {
reverseHashMap.remove(prevForwardValue, defaultReturnValue);
}
int prevReverseValue = this.reverseHashMap.get(value, defaultReturnValue);
this.reverseHashMap.put(value, key);
if (prevReverseValue != defaultReturnValue) {
forwardHashMap.remove(prevReverseValue);
}
return prevForwardValue;
}
/**
* Copies all of the mappings from the specified map to this map.
* These mappings will replace any mappings that this map had for
* any of the keys currently in the specified map.
*
* @param hashMap mappings to be stored in this map
*
* @throws NullPointerException if the specified map is null
*
* @throws IllegalArgumentException if the given value is already bound to a different key in this bimap. The bimap will remain
* unmodified in this event. To avoid this exception, call {@link #putAllForce(Map)} instead.
*/
public synchronized
void putAll(final Map<Integer, V> hashMap) throws IllegalArgumentException {
LockFreeIntBiMap<V> biMap = new LockFreeIntBiMap<V>();
try {
for (Map.Entry<Integer, V> entry : hashMap.entrySet()) {
Integer key = entry.getKey();
V value = entry.getValue();
biMap.put(key, value);
// we have to verify that the keys/values between the bimaps are unique
if (this.forwardHashMap.containsKey(key)) {
throw new IllegalArgumentException("Key already exists. Keys and values must both be unique!");
}
if (this.reverseHashMap.containsKey(value)) {
throw new IllegalArgumentException("Value already exists. Keys and values must both be unique!");
}
}
} catch (IllegalArgumentException e) {
// do nothing if there is an exception
throw e;
}
// we have checked to make sure that the bimap is unique, AND have checked that we don't already have any of the key/values in ourselves
this.forwardHashMap.putAll(biMap.forwardHashMap);
this.reverseHashMap.putAll(biMap.reverseHashMap);
}
/**
* Copies all of the mappings from the specified map to this map.
* These mappings will replace any mappings that this map had for
* any of the keys currently in the specified map. This is an alternate
* form of {@link #putAll(Map)} putAll(K, V) that will silently
* ignore duplicates
*
* @param hashMap mappings to be stored in this map
*
* @throws NullPointerException if the specified map is null
*/
public synchronized
void putAllForce(final Map<Integer, V> hashMap) {
for (Map.Entry<Integer, V> entry : hashMap.entrySet()) {
Integer key = entry.getKey();
V value = entry.getValue();
putForce(key, value);
}
}
/**
* Removes the mapping for the specified key from this map if present.
*
* @param key key whose mapping is to be removed from the map
*
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public synchronized
V remove(final int key) {
V value = forwardHashMap.remove(key);
if (value != null) {
reverseHashMap.remove(value, defaultReturnValue);
}
return value;
}
/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
* <p>
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
* key.equals(k))}, then this method returns {@code v}; otherwise
* it returns {@code null}. (There can be at most one such mapping.)
* <p>
* <p>A return value of {@code null} does not <i>necessarily</i>
* indicate that the map contains no mapping for the key; it's also
* possible that the map explicitly maps the key to {@code null}.
* The {@link HashMap#containsKey containsKey} operation may be used to
* distinguish these two cases.
*
* @see #put(int, Object)
*/
@SuppressWarnings("unchecked")
public
V get(final int key) {
// use the SWP to get a lock-free get of the value
return (V) forwardREF.get(this).get(key);
}
/**
* Returns <tt>true</tt> if this bimap contains no key-value mappings.
*
* @return <tt>true</tt> if this bimap contains no key-value mappings
*/
public
boolean isEmpty() {
// use the SWP to get a lock-free get of the value
return forwardREF.get(this)
.size == 0;
}
/**
* Returns the number of key-value mappings in this map. If the
* map contains more than <tt>Integer.MAX_VALUE</tt> elements, returns
* <tt>Integer.MAX_VALUE</tt>.
*
* @return the number of key-value mappings in this map
*/
public
int size() {
// use the SWP to get a lock-free get of the value
return forwardREF.get(this)
.size;
}
/**
* DO NOT MODIFY THE MAP VIA THIS (unless you synchronize around it!) It will result in unknown object visibility!
*
* Returns an iterator for the keys in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration.
*/
public
Keys keys() {
return forwardREF.get(this)
.keys();
}
/**
* DO NOT MODIFY THE MAP VIA THIS (unless you synchronize around it!) It will result in unknown object visibility!
*
* Returns an iterator for the values in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration.
*/
@SuppressWarnings("unchecked")
public
Values<V> values() {
return forwardREF.get(this)
.values();
}
/**
* DO NOT MODIFY THE MAP VIA THIS (unless you synchronize around it!) It will result in unknown object visibility!
*
* Returns an iterator for the entries in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration.
*/
public
Entries entries() {
return forwardREF.get(this)
.entries();
}
/**
* Identity equals only!
*/
@Override
public
boolean equals(final Object o) {
return this == o;
}
@Override
public
int hashCode() {
int result = forwardREF.get(this).hashCode();
result = 31 * result + reverseREF.get(this).hashCode();
result = 31 * result + defaultReturnValue;
return result;
}
@Override
public
String toString() {
StringBuilder builder = new StringBuilder("LockFreeIntBiMap {");
Keys keys = keys();
Iterator<V> values = values();
while (keys.hasNext) {
builder.append(keys.next());
builder.append(" (")
.append(values.next())
.append("), ");
}
int length = builder.length();
if (length > 1) {
// delete the ', '
builder.delete(length - 2, length);
}
builder.append('}');
return builder.toString();
}
}

View File

@ -0,0 +1,218 @@
/*
* Copyright 2018 dorkbox, llc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.collections;
import java.io.Serializable;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import dorkbox.collections.IntMap.*;
/**
* This class uses the "single-writer-principle" for lock-free publication.
* <p>
* Since there are only 2 methods to guarantee that modifications can only be called one-at-a-time (either it is only called by
* one thread, or only one thread can access it at a time) -- we chose the 2nd option -- and use 'synchronized' to make sure that only
* one thread can access this modification methods at a time. Getting or checking the presence of values can then happen in a lock-free
* manner.
* <p>
* According to my benchmarks, this is approximately 25% faster than ConcurrentHashMap for (all types of) reads, and a lot slower for
* contended writes.
* <p>
* This data structure is for many-read/few-write scenarios
*
* This is an unordered map that uses int keys. This implementation is a cuckoo hash map using 3 hashes, random walking, and a small stash
* for problematic keys. Null values are allowed. No allocation is done except when growing the table size. <br>
* <br>
* This map performs very fast get, containsKey, and remove (typically O(1), worst case O(log(n))). Put may be a bit slower,
* depending on hash collisions. Load factors greater than 0.91 greatly increase the chances the map will have to rehash to the
* next higher POT size.
* @author Nathan Sweet
*/
@SuppressWarnings("unchecked")
public final
class LockFreeIntMap<V> implements Cloneable, Serializable {
// Recommended for best performance while adhering to the "single writer principle". Must be static-final
private static final AtomicReferenceFieldUpdater<LockFreeIntMap, IntMap> mapREF = AtomicReferenceFieldUpdater.newUpdater(
LockFreeIntMap.class,
IntMap.class,
"map");
private volatile IntMap<V> map;
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
// use-case 99% of the time)
/**
* Constructs an empty <tt>IntMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public
LockFreeIntMap() {
map = new IntMap<V>();
}
/**
* Constructs an empty <tt>IntMap</tt> with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
*
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public
LockFreeIntMap(int initialCapacity) {
map = new IntMap<V>(initialCapacity);
}
/**
* Constructs an empty <tt>IntMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
*
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public
LockFreeIntMap(int initialCapacity, float loadFactor) {
this.map = new IntMap<V>(initialCapacity, loadFactor);
}
public
int size() {
// use the SWP to get a lock-free get of the value
return mapREF.get(this)
.size;
}
public
boolean isEmpty() {
// use the SWP to get a lock-free get of the value
return mapREF.get(this)
.size == 0;
}
public
boolean containsKey(final int key) {
// use the SWP to get a lock-free get of the value
return mapREF.get(this)
.containsKey(key);
}
/**
* Returns true if the specified value is in the map. Note this traverses the entire map and compares every value, which may be
* an expensive operation.
*
* @param identity If true, uses == to compare the specified value with values in the map. If false, uses
* {@link #equals(Object)}.
*/
public
boolean containsValue(final Object value, boolean identity) {
// use the SWP to get a lock-free get of the value
return mapREF.get(this)
.containsValue(value, identity);
}
public
V get(final int key) {
// use the SWP to get a lock-free get of the value
return (V) mapREF.get(this)
.get(key);
}
public synchronized
V put(final int key, final V value) {
return map.put(key, value);
}
public synchronized
V remove(final int key) {
return map.remove(key);
}
public synchronized
void putAll(final IntMap<V> map) {
this.map.putAll(map);
}
/**
* DO NOT MODIFY THE MAP VIA THIS (unless you synchronize around it!) It will result in unknown object visibility!
*
* Returns an iterator for the keys in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration.
*/
public
Keys keys() {
return mapREF.get(this)
.keys();
}
/**
* DO NOT MODIFY THE MAP VIA THIS (unless you synchronize around it!) It will result in unknown object visibility!
*
* Returns an iterator for the values in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration.
*/
public
Values<V> values() {
return mapREF.get(this)
.values();
}
/**
* DO NOT MODIFY THE MAP VIA THIS (unless you synchronize around it!) It will result in unknown object visibility!
*
* Returns an iterator for the entries in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration.
*/
public
Entries entries() {
return mapREF.get(this)
.entries();
}
public synchronized
void clear() {
map.clear();
}
/**
* Identity equals only!
*/
@Override
public
boolean equals(final Object o) {
return this == o;
}
@Override
public
int hashCode() {
return mapREF.get(this)
.hashCode();
}
@Override
public
String toString() {
return mapREF.get(this)
.toString();
}
}

View File

@ -0,0 +1,212 @@
package dorkbox.collections;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
/**
* This class uses the "single-writer-principle" for lock-free publication.
* <p>
* Since there are only 2 methods to guarantee that modifications can only be called one-at-a-time (either it is only called by
* one thread, or only one thread can access it at a time) -- we chose the 2nd option -- and use 'synchronized' to make sure that only
* one thread can access this modification methods at a time. Getting or checking the presence of values can then happen in a lock-free
* manner.
* <p>
* According to my benchmarks, this is approximately 25% faster than ConcurrentHashMap for (all types of) reads, and a lot slower for
* contended writes.
* <p>
* This data structure is for many-read/few-write scenarios
*
* This is an unordered map that uses int keys. This implementation is a cuckoo hash map using 3 hashes, random walking, and a small stash
* for problematic keys. Null values are allowed. No allocation is done except when growing the table size. <br>
* <br>
* This map performs very fast get, containsKey, and remove (typically O(1), worst case O(log(n))). Put may be a bit slower,
* depending on hash collisions. Load factors greater than 0.91 greatly increase the chances the map will have to rehash to the
* next higher POT size.
*/
@SuppressWarnings("unchecked")
public
class LockFreeIntStringMap<V> {
private static final AtomicReferenceFieldUpdater<LockFreeIntStringMap, IntMap> mapREF = AtomicReferenceFieldUpdater.newUpdater(
LockFreeIntStringMap.class,
IntMap.class,
"map");
private volatile IntMap<V> map;
public LockFreeIntStringMap() {
this.map = new IntMap<>();
}
/**
* Constructs an empty <tt>IntMap</tt> with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
*
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public
LockFreeIntStringMap(int initialCapacity) {
map = new IntMap<>(initialCapacity);
}
/**
* Constructs an empty <tt>IntMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
*
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public
LockFreeIntStringMap(int initialCapacity, float loadFactor) {
this.map = new IntMap(initialCapacity, loadFactor);
}
public
int size() {
// use the SWP to get a lock-free get of the value
return mapREF.get(this)
.size;
}
public
boolean isEmpty() {
// use the SWP to get a lock-free get of the value
return mapREF.get(this)
.size == 0;
}
public
boolean containsKey(final int key) {
// use the SWP to get a lock-free get of the value
return mapREF.get(this)
.containsKey(key);
}
public
boolean containsKey(final String key) {
// use the SWP to get a lock-free get of the value
return mapREF.get(this)
.containsKey(key.hashCode());
}
/**
* Returns true if the specified value is in the map. Note this traverses the entire map and compares every value, which may be
* an expensive operation.
*
* @param identity If true, uses == to compare the specified value with values in the map. If false, uses
* {@link #equals(Object)}.
*/
public
boolean containsValue(final Object value, boolean identity) {
// use the SWP to get a lock-free get of the value
return mapREF.get(this)
.containsValue(value, identity);
}
public
V get(final int key) {
// use the SWP to get a lock-free get of the value
return (V) mapREF.get(this)
.get(key);
}
public
V get(final String key) {
// use the SWP to get a lock-free get of the value
return (V) mapREF.get(this)
.get(key.hashCode());
}
public synchronized
V put(final int key, final V value) {
return map.put(key, value);
}
public synchronized
V put(final String key, final V value) {
return map.put(key.hashCode(), value);
}
public synchronized
V remove(final int key) {
return map.remove(key);
}
public synchronized
V remove(final String key) {
return map.remove(key.hashCode());
}
public synchronized
void putAll(final IntMap<V> map) {
this.map.putAll(map);
}
/**
* DO NOT MODIFY THE MAP VIA THIS (unless you synchronize around it!) It will result in unknown object visibility!
*
* Returns an iterator for the keys in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link IntMap.Entries} constructor for nested or multi-threaded iteration.
*/
public
IntMap.Keys keys() {
return mapREF.get(this)
.keys();
}
/**
* DO NOT MODIFY THE MAP VIA THIS (unless you synchronize around it!) It will result in unknown object visibility!
*
* Returns an iterator for the values in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link IntMap.Entries} constructor for nested or multi-threaded iteration.
*/
public
IntMap.Values<V> values() {
return mapREF.get(this)
.values();
}
/**
* DO NOT MODIFY THE MAP VIA THIS (unless you synchronize around it!) It will result in unknown object visibility!
*
* Returns an iterator for the entries in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link IntMap.Entries} constructor for nested or multi-threaded iteration.
*/
public
IntMap.Entries<V> entries() {
return mapREF.get(this)
.entries();
}
public synchronized
void clear() {
map.clear();
}
/**
* Identity equals only!
*/
@Override
public
boolean equals(final Object o) {
return this == o;
}
@Override
public
int hashCode() {
return mapREF.get(this)
.hashCode();
}
@Override
public
String toString() {
return mapREF.get(this)
.toString();
}
}

View File

@ -0,0 +1,400 @@
/*
* Copyright 2018 dorkbox, llc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import dorkbox.collections.IntMap.*;
/**
* A bimap (or "bidirectional map") is a map that preserves the uniqueness of its values as well as that of its keys. This constraint
* enables bimaps to support an "inverse view", which is another bimap containing the same entries as this bimap but with reversed keys and values.
*
* This class uses the "single-writer-principle" for lock-free publication.
*
* Since there are only 2 methods to guarantee that modifications can only be called one-at-a-time (either it is only called by
* one thread, or only one thread can access it at a time) -- we chose the 2nd option -- and use 'synchronized' to make sure that only
* one thread can access this modification methods at a time. Getting or checking the presence of values can then happen in a lock-free
* manner.
*
* According to my benchmarks, this is approximately 25% faster than ConcurrentHashMap for (all types of) reads, and a lot slower for
* contended writes.
*
* This data structure is for many-read/few-write scenarios
*/
public
class LockFreeObjectIntBiMap<V> {
// Recommended for best performance while adhering to the "single writer principle". Must be static-final
private static final AtomicReferenceFieldUpdater<LockFreeObjectIntBiMap, ObjectIntMap> forwardREF = AtomicReferenceFieldUpdater.newUpdater(
LockFreeObjectIntBiMap.class,
ObjectIntMap.class,
"forwardHashMap");
private static final AtomicReferenceFieldUpdater<LockFreeObjectIntBiMap, IntMap> reverseREF = AtomicReferenceFieldUpdater.newUpdater(
LockFreeObjectIntBiMap.class,
IntMap.class,
"reverseHashMap");
private volatile ObjectIntMap<V> forwardHashMap;
private volatile IntMap<V> reverseHashMap;
private final int defaultReturnValue;
private final LockFreeIntBiMap<V> inverse;
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
// use-case 99% of the time)
/**
* Creates a new bimap using @{link Integer#MIN_VALUE}.
*/
public
LockFreeObjectIntBiMap() {
this(Integer.MIN_VALUE);
}
/**
* The default return value is used for various get/put operations on the IntMap/ObjectIntMap.
*
* @param defaultReturnValue value used for various get/put operations on the IntMap/ObjectIntMap.
*/
public
LockFreeObjectIntBiMap(int defaultReturnValue) {
this(new ObjectIntMap<V>(), new IntMap<V>(), defaultReturnValue);
}
/**
* The default return value is used for various get/put operations on the IntMap/ObjectIntMap.
*
* @param defaultReturnValue value used for various get/put operations on the IntMap/ObjectIntMap.
*/
LockFreeObjectIntBiMap(ObjectIntMap<V> forwardHashMap, IntMap<V> reverseHashMap, int defaultReturnValue) {
this.forwardHashMap = forwardHashMap;
this.reverseHashMap = reverseHashMap;
this.defaultReturnValue = defaultReturnValue;
this.inverse = new LockFreeIntBiMap<V>(reverseHashMap, forwardHashMap, defaultReturnValue, this);
}
LockFreeObjectIntBiMap(final ObjectIntMap<V> forwardHashMap,
final IntMap<V> reverseHashMap,
final int defaultReturnValue,
final LockFreeIntBiMap<V> inverse) {
this.forwardHashMap = forwardHashMap;
this.reverseHashMap = reverseHashMap;
this.defaultReturnValue = defaultReturnValue;
this.inverse = inverse;
}
/**
* Removes all of the mappings from this bimap.
*
* The bimap will be empty after this call returns.
*/
public synchronized
void clear() {
forwardHashMap.clear();
reverseHashMap.clear();
}
/**
* @return the inverse view of this bimap, which maps each of this bimap's values to its associated key.
*/
public
LockFreeIntBiMap<V> inverse() {
return inverse;
}
/**
* Associates the specified value with the specified key in this bimap.
* If the bimap previously contained a mapping for the key, the old
* value is replaced. If the given value is already bound to a different
* key in this bimap, the bimap will remain unmodified. To avoid throwing
* an exception, call {@link #putForce(Object, int)} instead.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
*
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*
* @throws IllegalArgumentException if the given value is already bound to a different key in this bimap. The bimap will remain
* unmodified in this event. To avoid this exception, call {@link #putForce(Object, int)} instead.
*/
public synchronized
int put(final V key, final int value) throws IllegalArgumentException {
int prevForwardValue = this.forwardHashMap.get(key, defaultReturnValue);
this.forwardHashMap.put(key, value);
if (prevForwardValue != defaultReturnValue) {
reverseHashMap.remove(prevForwardValue);
}
V prevReverseValue = this.reverseHashMap.put(value, key);
if (prevReverseValue != null) {
// put the old value back
if (prevForwardValue != defaultReturnValue) {
this.forwardHashMap.put(key, prevForwardValue);
}
else {
this.forwardHashMap.remove(key, defaultReturnValue);
}
this.reverseHashMap.put(value, prevReverseValue);
throw new IllegalArgumentException("Value already exists. Keys and values must both be unique!");
}
return prevForwardValue;
}
/**
* Associates the specified value with the specified key in this bimap.
* If the bimap previously contained a mapping for the key, the old
* value is replaced. This is an alternate form of {@link #put(Object, int)}
* that will silently ignore duplicates
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
*
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public synchronized
int putForce(final V key, final int value) {
int prevForwardValue = this.forwardHashMap.get(key, defaultReturnValue);
this.forwardHashMap.put(key, value);
if (prevForwardValue != defaultReturnValue) {
reverseHashMap.remove(prevForwardValue);
}
V prevReverseValue = this.reverseHashMap.get(value);
this.reverseHashMap.put(value, key);
if (prevReverseValue != null) {
forwardHashMap.remove(prevReverseValue, defaultReturnValue);
}
return prevForwardValue;
}
/**
* Copies all of the mappings from the specified map to this map.
* These mappings will replace any mappings that this map had for
* any of the keys currently in the specified map.
*
* @param hashMap mappings to be stored in this map
*
* @throws NullPointerException if the specified map is null
*
* @throws IllegalArgumentException if the given value is already bound to a different key in this bimap. The bimap will remain
* unmodified in this event. To avoid this exception, call {@link #putAllForce(Map)} instead.
*/
public synchronized
void putAll(final Map<V, Integer> hashMap) throws IllegalArgumentException {
LockFreeObjectIntBiMap<V> biMap = new LockFreeObjectIntBiMap<V>();
try {
for (Map.Entry<V, Integer> entry : hashMap.entrySet()) {
V key = entry.getKey();
Integer value = entry.getValue();
biMap.put(key, value);
// we have to verify that the keys/values between the bimaps are unique
if (this.forwardHashMap.containsKey(key)) {
throw new IllegalArgumentException("Key already exists. Keys and values must both be unique!");
}
if (this.reverseHashMap.containsKey(value)) {
throw new IllegalArgumentException("Value already exists. Keys and values must both be unique!");
}
}
} catch (IllegalArgumentException e) {
// do nothing if there is an exception
throw e;
}
// we have checked to make sure that the bimap is unique, AND have checked that we don't already have any of the key/values in ourselves
this.forwardHashMap.putAll(biMap.forwardHashMap);
this.reverseHashMap.putAll(biMap.reverseHashMap);
}
/**
* Copies all of the mappings from the specified map to this map.
* These mappings will replace any mappings that this map had for
* any of the keys currently in the specified map. This is an alternate
* form of {@link #putAll(Map)} putAll(K, V) that will silently
* ignore duplicates
*
* @param hashMap mappings to be stored in this map
*
* @throws NullPointerException if the specified map is null
*/
public synchronized
void putAllForce(final Map<V, Integer> hashMap) {
for (Map.Entry<V, Integer> entry : hashMap.entrySet()) {
V key = entry.getKey();
Integer value = entry.getValue();
putForce(key, value);
}
}
/**
* Removes the mapping for the specified key from this map if present.
*
* @param key key whose mapping is to be removed from the map
*
* @return the previous value associated with <tt>key</tt>, or
* <tt>defaultReturnValue</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>defaultReturnValue</tt> return can also indicate that the map
* previously associated <tt>defaultReturnValue</tt> with <tt>key</tt>.)
*/
public synchronized
int remove(final V key) {
int value = forwardHashMap.remove(key, defaultReturnValue);
if (value != defaultReturnValue) {
reverseHashMap.remove(value);
}
return value;
}
/**
* Returns the value to which the specified key is mapped,
* or {@code defaultReturnValue} if this map contains no mapping for the key.
* <p>
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
* key.equals(k))}, then this method returns {@code v}; otherwise
* it returns {@code defaultReturnValue}. (There can be at most one such mapping.)
* <p>
* <p>A return value of {@code defaultReturnValue} does not <i>necessarily</i>
* indicate that the map contains no mapping for the key; it's also
* possible that the map explicitly maps the key to {@code null}.
* The {@link HashMap#containsKey containsKey} operation may be used to
* distinguish these two cases.
*
* @see #put(Object, int)
*/
@SuppressWarnings("unchecked")
public
int get(final V key) {
// use the SWP to get a lock-free get of the value
return forwardREF.get(this).get(key, defaultReturnValue);
}
/**
* Returns an iterator for the keys in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration.
*/
@SuppressWarnings("unchecked")
public
Iterator<V> keys() {
// the ObjectIntMap doesn't have iterators, but the IntMap does
return inverse.values();
}
/**
* Returns an iterator for the values in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration.
*/
@SuppressWarnings("unchecked")
public
Keys values() {
// the ObjectIntMap doesn't have iterators, but the IntMap does
return inverse.keys();
}
/**
* Returns <tt>true</tt> if this bimap contains no key-value mappings.
*
* @return <tt>true</tt> if this bimap contains no key-value mappings
*/
public
boolean isEmpty() {
// use the SWP to get a lock-free get of the value
return forwardREF.get(this)
.size == 0;
}
/**
* Returns the number of key-value mappings in this map. If the
* map contains more than <tt>Integer.MAX_VALUE</tt> elements, returns
* <tt>Integer.MAX_VALUE</tt>.
*
* @return the number of key-value mappings in this map
*/
public
int size() {
// use the SWP to get a lock-free get of the value
return forwardREF.get(this)
.size;
}
/**
* Identity equals only!
*/
@Override
public
boolean equals(final Object o) {
return this == o;
}
@Override
public
int hashCode() {
int result = forwardREF.get(this).hashCode();
result = 31 * result + reverseREF.get(this).hashCode();
result = 31 * result + defaultReturnValue;
return result;
}
@Override
public
String toString() {
StringBuilder builder = new StringBuilder("LockFreeObjectIntBiMap {");
Iterator<V> keys = keys();
Keys values = values();
while (keys.hasNext()) {
builder.append(keys.next());
builder.append(" (")
.append(values.next())
.append("), ");
}
int length = builder.length();
if (length > 1) {
// delete the ', '
builder.delete(length - 2, length);
}
builder.append('}');
return builder.toString();
}
}

View File

@ -0,0 +1,210 @@
/*
* Copyright 2018 dorkbox, llc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
/**
* This class uses the "single-writer-principle" for lock-free publication.
*
* Since there are only 2 methods to guarantee that modifications can only be called one-at-a-time (either it is only called by
* one thread, or only one thread can access it at a time) -- we chose the 2nd option -- and use 'synchronized' to make sure that only
* one thread can access this modification methods at a time. Getting or checking the presence of values can then happen in a lock-free
* manner.
*
* According to my benchmarks, this is approximately 25% faster than ConcurrentHashMap for (all types of) reads, and a lot slower for
* contended writes.
*
* This data structure is for many-read/few-write scenarios
*/
public
class LockFreeObjectIntMap<V> {
// Recommended for best performance while adhering to the "single writer principle". Must be static-final
private static final AtomicReferenceFieldUpdater<LockFreeObjectIntMap, ObjectIntMap> mapREF = AtomicReferenceFieldUpdater.newUpdater(
LockFreeObjectIntMap.class,
ObjectIntMap.class,
"map");
private volatile ObjectIntMap<V> map;
private final int defaultReturnValue;
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
// use-case 99% of the time)
/**
* Creates a new map using @{link Integer#MIN_VALUE}.
*/
public
LockFreeObjectIntMap() {
this(Integer.MIN_VALUE);
}
/**
* The default return value is used for various get/put operations on the ObjectIntMap.
*
* @param defaultReturnValue value used for various get/put operations on the ObjectIntMap.
*/
public
LockFreeObjectIntMap(int defaultReturnValue) {
this(new ObjectIntMap<V>(), defaultReturnValue);
}
/**
* The default return value is used for various get/put operations on the ObjectIntMap.
*
* @param defaultReturnValue value used for various get/put operations on the ObjectIntMap.
*/
LockFreeObjectIntMap(ObjectIntMap<V> forwardHashMap, int defaultReturnValue) {
this.map = forwardHashMap;
this.defaultReturnValue = defaultReturnValue;
}
/**
* Removes all of the mappings from this map.
*
* The map will be empty after this call returns.
*/
public synchronized
void clear() {
map.clear();
}
public synchronized
int put(final V key, final int value) {
int prevForwardValue = this.map.get(key, defaultReturnValue);
this.map.put(key, value);
return prevForwardValue;
}
/**
* Copies all of the mappings from the specified map to this map.
* These mappings will replace any mappings that this map had for
* any of the keys currently in the specified map.
*
* @param hashMap mappings to be stored in this map
*
* @throws NullPointerException if the specified map is null
*/
public synchronized
void putAll(final Map<V, Integer> hashMap) throws IllegalArgumentException {
try {
ObjectIntMap<V> map = this.map;
for (Map.Entry<V, Integer> entry : hashMap.entrySet()) {
V key = entry.getKey();
Integer value = entry.getValue();
map.put(key, value);
}
} catch (IllegalArgumentException e) {
// do nothing if there is an exception
throw e;
}
}
/**
* Removes the mapping for the specified key from this map if present.
*
* @param key key whose mapping is to be removed from the map
*
* @return the previous value associated with <tt>key</tt>, or
* <tt>defaultReturnValue</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>defaultReturnValue</tt> return can also indicate that the map
* previously associated <tt>defaultReturnValue</tt> with <tt>key</tt>.)
*/
public synchronized
int remove(final V key) {
int value = map.remove(key, defaultReturnValue);
return value;
}
/**
* Returns the value to which the specified key is mapped,
* or {@code defaultReturnValue} if this map contains no mapping for the key.
* <p>
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
* key.equals(k))}, then this method returns {@code v}; otherwise
* it returns {@code defaultReturnValue}. (There can be at most one such mapping.)
* <p>
* <p>A return value of {@code defaultReturnValue} does not <i>necessarily</i>
* indicate that the map contains no mapping for the key; it's also
* possible that the map explicitly maps the key to {@code null}.
* The {@link HashMap#containsKey containsKey} operation may be used to
* distinguish these two cases.
*
* @see #put(Object, int)
*/
@SuppressWarnings("unchecked")
public
int get(final V key) {
// use the SWP to get a lock-free get of the value
return mapREF.get(this).get(key, defaultReturnValue);
}
/**
* Returns <tt>true</tt> if this map contains no key-value mappings.
*
* @return <tt>true</tt> if this map contains no key-value mappings
*/
public
boolean isEmpty() {
// use the SWP to get a lock-free get of the value
return mapREF.get(this)
.size == 0;
}
/**
* Returns the number of key-value mappings in this map. If the
* map contains more than <tt>Integer.MAX_VALUE</tt> elements, returns
* <tt>Integer.MAX_VALUE</tt>.
*
* @return the number of key-value mappings in this map
*/
public
int size() {
// use the SWP to get a lock-free get of the value
return mapREF.get(this)
.size;
}
/**
* Identity equals only!
*/
@Override
public
boolean equals(final Object o) {
return this == o;
}
@Override
public
int hashCode() {
return mapREF.get(this).hashCode();
}
@Override
public
String toString() {
return mapREF.get(this)
.toString();
}
}

View File

@ -0,0 +1,200 @@
/*
* Copyright 2018 dorkbox, llc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.collections;
import java.io.Serializable;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
/**
* This class uses the "single-writer-principle" for lock-free publication.
* <p>
* Since there are only 2 methods to guarantee that modifications can only be called one-at-a-time (either it is only called by
* one thread, or only one thread can access it at a time) -- we chose the 2nd option -- and use 'synchronized' to make sure that only
* one thread can access this modification methods at a time. Getting or checking the presence of values can then happen in a lock-free
* manner.
* <p>
* According to my benchmarks, this is approximately 25% faster than ConcurrentHashMap for (all types of) reads, and a lot slower for
* contended writes.
* <p>
* This data structure is for many-read/few-write scenarios
*/
@SuppressWarnings("unchecked")
public final
class LockFreeObjectMap<K, V> implements Cloneable, Serializable {
// Recommended for best performance while adhering to the "single writer principle". Must be static-final
private static final AtomicReferenceFieldUpdater<LockFreeObjectMap, ObjectMap> mapREF = AtomicReferenceFieldUpdater.newUpdater(
LockFreeObjectMap.class,
ObjectMap.class,
"hashMap");
private volatile ObjectMap<K, V> hashMap;
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
// use-case 99% of the time)
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public
LockFreeObjectMap() {
hashMap = new ObjectMap<K, V>();
}
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
*
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public
LockFreeObjectMap(int initialCapacity) {
hashMap = new ObjectMap<K, V>(initialCapacity);
}
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
*
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public
LockFreeObjectMap(int initialCapacity, float loadFactor) {
this.hashMap = new ObjectMap<K, V>(initialCapacity, loadFactor);
}
public
int size() {
// use the SWP to get a lock-free get of the value
return mapREF.get(this)
.size;
}
public
boolean isEmpty() {
// use the SWP to get a lock-free get of the value
return mapREF.get(this)
.size == 0;
}
public
boolean containsKey(final K key) {
// use the SWP to get a lock-free get of the value
return mapREF.get(this)
.containsKey(key);
}
public
boolean containsValue(final V value, boolean identity) {
// use the SWP to get a lock-free get of the value
return mapREF.get(this)
.containsValue(value, identity);
}
@SuppressWarnings("unchecked")
public
V get(final K key) {
// use the SWP to get a lock-free get of the value
return (V) mapREF.get(this)
.get(key);
}
public synchronized
V put(final K key, final V value) {
return hashMap.put(key, value);
}
public synchronized
V remove(final K key) {
return hashMap.remove(key);
}
public synchronized
void putAll(final ObjectMap<K, V> map) {
this.hashMap.putAll(map);
}
public synchronized
void clear() {
hashMap.clear();
}
/**
* DO NOT MODIFY THE MAP VIA THIS (unless you synchronize around it!) It will result in unknown object visibility!
*
* Returns an iterator for the keys in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link ObjectMap.Entries} constructor for nested or multithreaded iteration.
*/
public
ObjectMap.Keys keys() {
return mapREF.get(this)
.keys();
}
/**
* DO NOT MODIFY THE MAP VIA THIS (unless you synchronize around it!) It will result in unknown object visibility!
*
* Returns an iterator for the values in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link ObjectMap.Entries} constructor for nested or multithreaded iteration.
*/
public
ObjectMap.Values values() {
return mapREF.get(this)
.values();
}
/**
* DO NOT MODIFY THE MAP VIA THIS (unless you synchronize around it!) It will result in unknown object visibility!
*
* Returns an iterator for the entries in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link ObjectMap.Entries} constructor for nested or multithreaded iteration.
*/
public
ObjectMap.Entries entries() {
return mapREF.get(this)
.entries();
}
/**
* Identity equals only!
*/
@Override
public
boolean equals(final Object o) {
return this == o;
}
@Override
public
int hashCode() {
return mapREF.get(this)
.hashCode();
}
@Override
public
String toString() {
return mapREF.get(this)
.toString();
}
}

View File

@ -0,0 +1,220 @@
/*
* Copyright 2015 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.collections;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
/**
* This class uses the "single-writer-principle" for lock-free publication.
* <p>
* Since there are only 2 methods to guarantee that modifications can only be called one-at-a-time (either it is only called by
* one thread, or only one thread can access it at a time) -- we chose the 2nd option -- and use 'synchronized' to make sure that only
* one thread can access this modification methods at a time. Getting or checking the presence of values can then happen in a lock-free
* manner.
* <p>
* According to my benchmarks, this is approximately 25% faster than ConcurrentHashMap for (all types of) reads, and a lot slower for
* contended writes.
* <p>
* This data structure is for many-read/few-write scenarios
*/
@SuppressWarnings("unchecked")
public final
class LockFreeSet<E> implements Set<E>, Cloneable, java.io.Serializable {
// Recommended for best performance while adhering to the "single writer principle". Must be static-final
private static final AtomicReferenceFieldUpdater<LockFreeSet, Set> setREF = AtomicReferenceFieldUpdater.newUpdater(LockFreeSet.class,
Set.class,
"hashSet");
private volatile Set<E> hashSet;
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
// use-case 99% of the time)
/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* default initial capacity (16) and load factor (0.75).
*/
public
LockFreeSet() {
hashSet = new HashSet<E>();
}
/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* the specified initial capacity and the specified load factor.
*
* @param initialCapacity the initial capacity of the hash map
* @param loadFactor the load factor of the hash map
*
* @throws IllegalArgumentException if the initial capacity is less
* than zero, or if the load factor is nonpositive
*/
public
LockFreeSet(int initialCapacity, float loadFactor) {
hashSet = new HashSet<E>(initialCapacity, loadFactor);
}
/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* the specified initial capacity and default load factor (0.75).
*
* @param initialCapacity the initial capacity of the hash table
*
* @throws IllegalArgumentException if the initial capacity is less
* than zero
*/
public
LockFreeSet(int initialCapacity) {
hashSet = new HashSet<E>(initialCapacity);
}
/**
* Constructs a new set containing the elements in the specified
* collection. The <tt>HashMap</tt> is created with default load factor
* (0.75) and an initial capacity sufficient to contain the elements in
* the specified collection.
*
* @param collection the collection whose elements are to be placed into this set
*
* @throws NullPointerException if the specified collection is null
*/
public
LockFreeSet(final Collection<E> collection) {
hashSet = new HashSet<E>(collection);
}
@SuppressWarnings("unchecked")
public
Set<E> elements() {
// use the SWP to get a lock-free get of the value
return Collections.unmodifiableSet(setREF.get(this));
}
@Override
public
int size() {
return setREF.get(this)
.size();
}
@Override
public
boolean isEmpty() {
// use the SWP to get a lock-free get of the value
return setREF.get(this)
.isEmpty();
}
@Override
public
boolean contains(final Object element) {
// use the SWP to get a lock-free get of the value
return setREF.get(this)
.contains(element);
}
@Override
public
Iterator<E> iterator() {
return elements().iterator();
}
@Override
public
Object[] toArray() {
return setREF.get(this)
.toArray();
}
@Override
public
<T> T[] toArray(final T[] a) {
return (T[]) setREF.get(this)
.toArray(a);
}
@Override
public synchronized
boolean add(final E element) {
return hashSet.add(element);
}
@Override
public synchronized
boolean remove(final Object element) {
return hashSet.remove(element);
}
@Override
public
boolean containsAll(final Collection<?> collection) {
// use the SWP to get a lock-free get of the value
return setREF.get(this)
.containsAll(collection);
}
@Override
public synchronized
boolean addAll(final Collection<? extends E> elements) {
return hashSet.addAll(elements);
}
@Override
public synchronized
boolean retainAll(final Collection<?> collection) {
return hashSet.retainAll(collection);
}
@Override
public synchronized
boolean removeAll(final Collection<?> collection) {
return hashSet.removeAll(collection);
}
@Override
public synchronized
void clear() {
hashSet.clear();
}
@Override
public
boolean equals(final Object o) {
return setREF.get(this).equals(o);
}
@Override
public
int hashCode() {
return setREF.get(this)
.hashCode();
}
@Override
public
String toString() {
return setREF.get(this)
.toString();
}
}

View File

@ -0,0 +1,408 @@
/*******************************************************************************
* Copyright 2011 LibGDX.
* Mario Zechner <badlogicgames@gmail.com>
* Nathan Sweet <nathan.sweet@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package dorkbox.collections;
import java.util.Arrays;
/** A resizable, ordered or unordered long array. Avoids the boxing that occurs with ArrayList<Long>. If unordered, this class
* avoids a memory copy when removing elements (the last element is moved to the removed element's position).
* @author Nathan Sweet */
public class LongArray {
public long[] items;
public int size;
public boolean ordered;
/** Creates an ordered array with a capacity of 16. */
public LongArray () {
this(true, 16);
}
/** Creates an ordered array with the specified capacity. */
public LongArray (int capacity) {
this(true, capacity);
}
/** @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a
* memory copy.
* @param capacity Any elements added beyond this will cause the backing array to be grown. */
public LongArray (boolean ordered, int capacity) {
this.ordered = ordered;
items = new long[capacity];
}
/** Creates a new array containing the elements in the specific array. The new array will be ordered if the specific array is
* ordered. The capacity is set to the number of elements, so any subsequent elements added will cause the backing array to be
* grown. */
public LongArray (LongArray array) {
this.ordered = array.ordered;
size = array.size;
items = new long[size];
System.arraycopy(array.items, 0, items, 0, size);
}
/** Creates a new ordered array containing the elements in the specified array. The capacity is set to the number of elements,
* so any subsequent elements added will cause the backing array to be grown. */
public LongArray (long[] array) {
this(true, array, 0, array.length);
}
/** Creates a new array containing the elements in the specified array. The capacity is set to the number of elements, so any
* subsequent elements added will cause the backing array to be grown.
* @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a
* memory copy. */
public LongArray (boolean ordered, long[] array, int startIndex, int count) {
this(ordered, count);
size = count;
System.arraycopy(array, startIndex, items, 0, count);
}
public void add (long value) {
long[] items = this.items;
if (size == items.length) items = resize(Math.max(8, (int)(size * 1.75f)));
items[size++] = value;
}
public void add (long value1, long value2) {
long[] items = this.items;
if (size + 1 >= items.length) items = resize(Math.max(8, (int)(size * 1.75f)));
items[size] = value1;
items[size + 1] = value2;
size += 2;
}
public void add (long value1, long value2, long value3) {
long[] items = this.items;
if (size + 2 >= items.length) items = resize(Math.max(8, (int)(size * 1.75f)));
items[size] = value1;
items[size + 1] = value2;
items[size + 2] = value3;
size += 3;
}
public void add (long value1, long value2, long value3, long value4) {
long[] items = this.items;
if (size + 3 >= items.length) items = resize(Math.max(8, (int)(size * 1.8f))); // 1.75 isn't enough when size=5.
items[size] = value1;
items[size + 1] = value2;
items[size + 2] = value3;
items[size + 3] = value4;
size += 4;
}
public void addAll (LongArray array) {
addAll(array.items, 0, array.size);
}
public void addAll (LongArray array, int offset, int length) {
if (offset + length > array.size)
throw new IllegalArgumentException("offset + length must be <= size: " + offset + " + " + length + " <= " + array.size);
addAll(array.items, offset, length);
}
public void addAll (long... array) {
addAll(array, 0, array.length);
}
public void addAll (long[] array, int offset, int length) {
long[] items = this.items;
int sizeNeeded = size + length;
if (sizeNeeded > items.length) items = resize(Math.max(8, (int)(sizeNeeded * 1.75f)));
System.arraycopy(array, offset, items, size, length);
size += length;
}
public long get (int index) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
return items[index];
}
public void set (int index, long value) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
items[index] = value;
}
public void incr (int index, long value) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
items[index] += value;
}
public void mul (int index, long value) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
items[index] *= value;
}
public void insert (int index, long value) {
if (index > size) throw new IndexOutOfBoundsException("index can't be > size: " + index + " > " + size);
long[] items = this.items;
if (size == items.length) items = resize(Math.max(8, (int)(size * 1.75f)));
if (ordered)
System.arraycopy(items, index, items, index + 1, size - index);
else
items[size] = items[index];
size++;
items[index] = value;
}
public void swap (int first, int second) {
if (first >= size) throw new IndexOutOfBoundsException("first can't be >= size: " + first + " >= " + size);
if (second >= size) throw new IndexOutOfBoundsException("second can't be >= size: " + second + " >= " + size);
long[] items = this.items;
long firstValue = items[first];
items[first] = items[second];
items[second] = firstValue;
}
public boolean contains (long value) {
int i = size - 1;
long[] items = this.items;
while (i >= 0)
if (items[i--] == value) return true;
return false;
}
public int indexOf (long value) {
long[] items = this.items;
for (int i = 0, n = size; i < n; i++)
if (items[i] == value) return i;
return -1;
}
public int lastIndexOf (char value) {
long[] items = this.items;
for (int i = size - 1; i >= 0; i--)
if (items[i] == value) return i;
return -1;
}
public boolean removeValue (long value) {
long[] items = this.items;
for (int i = 0, n = size; i < n; i++) {
if (items[i] == value) {
removeIndex(i);
return true;
}
}
return false;
}
/** Removes and returns the item at the specified index. */
public long removeIndex (int index) {
if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size);
long[] items = this.items;
long value = items[index];
size--;
if (ordered)
System.arraycopy(items, index + 1, items, index, size - index);
else
items[index] = items[size];
return value;
}
/** Removes the items between the specified indices, inclusive. */
public void removeRange (int start, int end) {
if (end >= size) throw new IndexOutOfBoundsException("end can't be >= size: " + end + " >= " + size);
if (start > end) throw new IndexOutOfBoundsException("start can't be > end: " + start + " > " + end);
long[] items = this.items;
int count = end - start + 1;
if (ordered)
System.arraycopy(items, start + count, items, start, size - (start + count));
else {
int lastIndex = this.size - 1;
for (int i = 0; i < count; i++)
items[start + i] = items[lastIndex - i];
}
size -= count;
}
/** Removes from this array all of elements contained in the specified array.
* @return true if this array was modified. */
public boolean removeAll (LongArray array) {
int size = this.size;
int startSize = size;
long[] items = this.items;
for (int i = 0, n = array.size; i < n; i++) {
long item = array.get(i);
for (int ii = 0; ii < size; ii++) {
if (item == items[ii]) {
removeIndex(ii);
size--;
break;
}
}
}
return size != startSize;
}
/** Removes and returns the last item. */
public long pop () {
return items[--size];
}
/** Returns the last item. */
public long peek () {
return items[size - 1];
}
/** Returns the first item. */
public long first () {
if (size == 0) throw new IllegalStateException("Array is empty.");
return items[0];
}
/** Returns true if the array is empty. */
public boolean isEmpty () {
return size == 0;
}
public void clear () {
size = 0;
}
/** Reduces the size of the backing array to the size of the actual items. This is useful to release memory when many items
* have been removed, or if it is known that more items will not be added.
* @return {@link #items} */
public long[] shrink () {
if (items.length != size) resize(size);
return items;
}
/** Increases the size of the backing array to accommodate the specified number of additional items. Useful before adding many
* items to avoid multiple backing array resizes.
* @return {@link #items} */
public long[] ensureCapacity (int additionalCapacity) {
if (additionalCapacity < 0) throw new IllegalArgumentException("additionalCapacity must be >= 0: " + additionalCapacity);
int sizeNeeded = size + additionalCapacity;
if (sizeNeeded > items.length) resize(Math.max(8, sizeNeeded));
return items;
}
/** Sets the array size, leaving any values beyond the current size undefined.
* @return {@link #items} */
public long[] setSize (int newSize) {
if (newSize < 0) throw new IllegalArgumentException("newSize must be >= 0: " + newSize);
if (newSize > items.length) resize(Math.max(8, newSize));
size = newSize;
return items;
}
protected long[] resize (int newSize) {
long[] newItems = new long[newSize];
long[] items = this.items;
System.arraycopy(items, 0, newItems, 0, Math.min(size, newItems.length));
this.items = newItems;
return newItems;
}
public void sort () {
Arrays.sort(items, 0, size);
}
public void reverse () {
long[] items = this.items;
for (int i = 0, lastIndex = size - 1, n = size / 2; i < n; i++) {
int ii = lastIndex - i;
long temp = items[i];
items[i] = items[ii];
items[ii] = temp;
}
}
public void shuffle () {
long[] items = this.items;
for (int i = size - 1; i >= 0; i--) {
int ii = MathUtil.random(i);
long temp = items[i];
items[i] = items[ii];
items[ii] = temp;
}
}
/** Reduces the size of the array to the specified size. If the array is already smaller than the specified size, no action is
* taken. */
public void truncate (int newSize) {
if (size > newSize) size = newSize;
}
/** Returns a random item from the array, or zero if the array is empty. */
public long random () {
if (size == 0) return 0;
return items[MathUtil.random(0,size - 1)];
}
public long[] toArray () {
long[] array = new long[size];
System.arraycopy(items, 0, array, 0, size);
return array;
}
public int hashCode () {
if (!ordered) return super.hashCode();
long[] items = this.items;
int h = 1;
for (int i = 0, n = size; i < n; i++)
h = h * 31 + (int)(items[i] ^ (items[i] >>> 32));
return h;
}
public boolean equals (Object object) {
if (object == this) return true;
if (!ordered) return false;
if (!(object instanceof LongArray)) return false;
LongArray array = (LongArray)object;
if (!array.ordered) return false;
int n = size;
if (n != array.size) return false;
long[] items1 = this.items;
long[] items2 = array.items;
for (int i = 0; i < n; i++)
if (items[i] != array.items[i]) return false;
return true;
}
public String toString () {
if (size == 0) return "[]";
long[] items = this.items;
StringBuilder buffer = new StringBuilder(32);
buffer.append('[');
buffer.append(items[0]);
for (int i = 1; i < size; i++) {
buffer.append(", ");
buffer.append(items[i]);
}
buffer.append(']');
return buffer.toString();
}
public String toString (String separator) {
if (size == 0) return "";
long[] items = this.items;
StringBuilder buffer = new StringBuilder(32);
buffer.append(items[0]);
for (int i = 1; i < size; i++) {
buffer.append(separator);
buffer.append(items[i]);
}
return buffer.toString();
}
/** @see #LongArray(long[]) */
static public LongArray with (long... array) {
return new LongArray(array);
}
}

View File

@ -0,0 +1,871 @@
/*******************************************************************************
* Copyright 2011 LibGDX.
* Mario Zechner <badlogicgames@gmail.com>
* Nathan Sweet <nathan.sweet@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package dorkbox.collections;
import java.util.Iterator;
import java.util.NoSuchElementException;
/** An unordered map that uses long keys. This implementation is a cuckoo hash map using 3 hashes, random walking, and a small
* stash for problematic keys. Null values are allowed. No allocation is done except when growing the table size. <br>
* <br>
* This map performs very fast get, containsKey, and remove (typically O(1), worst case O(log(n))). Put may be a bit slower,
* depending on hash collisions. Load factors greater than 0.91 greatly increase the chances the map will have to rehash to the
* next higher POT size.
* @author Nathan Sweet */
@SuppressWarnings({"NullableProblems", "rawtypes", "unchecked"})
public class LongMap<V> implements Iterable<LongMap.Entry<V>> {
private static final int PRIME1 = 0xbe1f14b1;
private static final int PRIME2 = 0xb4b82e39;
private static final int PRIME3 = 0xced1c241;
private static final int EMPTY = 0;
public int size;
long[] keyTable;
V[] valueTable;
int capacity, stashSize;
V zeroValue;
boolean hasZeroValue;
private float loadFactor;
private int hashShift, mask, threshold;
private int stashCapacity;
private int pushIterations;
private Entries entries1, entries2;
private Values values1, values2;
private Keys keys1, keys2;
/** Creates a new map with an initial capacity of 51 and a load factor of 0.8. */
public LongMap () {
this(51, 0.8f);
}
/** Creates a new map with a load factor of 0.8.
* @param initialCapacity If not a power of two, it is increased to the next nearest power of two. */
public LongMap (int initialCapacity) {
this(initialCapacity, 0.8f);
}
/** Creates a new map with the specified initial capacity and load factor. This map will hold initialCapacity items before
* growing the backing table.
* @param initialCapacity If not a power of two, it is increased to the next nearest power of two. */
public LongMap (int initialCapacity, float loadFactor) {
if (initialCapacity < 0) throw new IllegalArgumentException("initialCapacity must be >= 0: " + initialCapacity);
initialCapacity = MathUtil.nextPowerOfTwo((int)Math.ceil(initialCapacity / loadFactor));
if (initialCapacity > 1 << 30) throw new IllegalArgumentException("initialCapacity is too large: " + initialCapacity);
capacity = initialCapacity;
if (loadFactor <= 0) throw new IllegalArgumentException("loadFactor must be > 0: " + loadFactor);
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
mask = capacity - 1;
hashShift = 63 - Long.numberOfTrailingZeros(capacity);
stashCapacity = Math.max(3, (int)Math.ceil(Math.log(capacity)) * 2);
pushIterations = Math.max(Math.min(capacity, 8), (int)Math.sqrt(capacity) / 8);
keyTable = new long[capacity + stashCapacity];
valueTable = (V[])new Object[keyTable.length];
}
/** Creates a new map identical to the specified map. */
public LongMap (LongMap<? extends V> map) {
this((int)Math.floor(map.capacity * map.loadFactor), map.loadFactor);
stashSize = map.stashSize;
System.arraycopy(map.keyTable, 0, keyTable, 0, map.keyTable.length);
System.arraycopy(map.valueTable, 0, valueTable, 0, map.valueTable.length);
size = map.size;
zeroValue = map.zeroValue;
hasZeroValue = map.hasZeroValue;
}
public V put (long key, V value) {
if (key == 0) {
V oldValue = zeroValue;
zeroValue = value;
if (!hasZeroValue) {
hasZeroValue = true;
size++;
}
return oldValue;
}
long[] keyTable = this.keyTable;
// Check for existing keys.
int index1 = (int)(key & mask);
long key1 = keyTable[index1];
if (key1 == key) {
V oldValue = valueTable[index1];
valueTable[index1] = value;
return oldValue;
}
int index2 = hash2(key);
long key2 = keyTable[index2];
if (key2 == key) {
V oldValue = valueTable[index2];
valueTable[index2] = value;
return oldValue;
}
int index3 = hash3(key);
long key3 = keyTable[index3];
if (key3 == key) {
V oldValue = valueTable[index3];
valueTable[index3] = value;
return oldValue;
}
// Update key in the stash.
for (int i = capacity, n = i + stashSize; i < n; i++) {
if (keyTable[i] == key) {
V oldValue = valueTable[i];
valueTable[i] = value;
return oldValue;
}
}
// Check for empty buckets.
if (key1 == EMPTY) {
keyTable[index1] = key;
valueTable[index1] = value;
if (size++ >= threshold) resize(capacity << 1);
return null;
}
if (key2 == EMPTY) {
keyTable[index2] = key;
valueTable[index2] = value;
if (size++ >= threshold) resize(capacity << 1);
return null;
}
if (key3 == EMPTY) {
keyTable[index3] = key;
valueTable[index3] = value;
if (size++ >= threshold) resize(capacity << 1);
return null;
}
push(key, value, index1, key1, index2, key2, index3, key3);
return null;
}
public void putAll (LongMap<? extends V> map) {
for (Entry<? extends V> entry : map.entries())
put(entry.key, entry.value);
}
/** Skips checks for existing keys. */
private void putResize (long key, V value) {
if (key == 0) {
zeroValue = value;
hasZeroValue = true;
return;
}
// Check for empty buckets.
int index1 = (int)(key & mask);
long key1 = keyTable[index1];
if (key1 == EMPTY) {
keyTable[index1] = key;
valueTable[index1] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
int index2 = hash2(key);
long key2 = keyTable[index2];
if (key2 == EMPTY) {
keyTable[index2] = key;
valueTable[index2] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
int index3 = hash3(key);
long key3 = keyTable[index3];
if (key3 == EMPTY) {
keyTable[index3] = key;
valueTable[index3] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
push(key, value, index1, key1, index2, key2, index3, key3);
}
private void push (long insertKey, V insertValue, int index1, long key1, int index2, long key2, int index3, long key3) {
long[] keyTable = this.keyTable;
V[] valueTable = this.valueTable;
int mask = this.mask;
// Push keys until an empty bucket is found.
long evictedKey;
V evictedValue;
int i = 0, pushIterations = this.pushIterations;
do {
// Replace the key and value for one of the hashes.
switch (MathUtil.random(2)) {
case 0:
evictedKey = key1;
evictedValue = valueTable[index1];
keyTable[index1] = insertKey;
valueTable[index1] = insertValue;
break;
case 1:
evictedKey = key2;
evictedValue = valueTable[index2];
keyTable[index2] = insertKey;
valueTable[index2] = insertValue;
break;
default:
evictedKey = key3;
evictedValue = valueTable[index3];
keyTable[index3] = insertKey;
valueTable[index3] = insertValue;
break;
}
// If the evicted key hashes to an empty bucket, put it there and stop.
index1 = (int)(evictedKey & mask);
key1 = keyTable[index1];
if (key1 == EMPTY) {
keyTable[index1] = evictedKey;
valueTable[index1] = evictedValue;
if (size++ >= threshold) resize(capacity << 1);
return;
}
index2 = hash2(evictedKey);
key2 = keyTable[index2];
if (key2 == EMPTY) {
keyTable[index2] = evictedKey;
valueTable[index2] = evictedValue;
if (size++ >= threshold) resize(capacity << 1);
return;
}
index3 = hash3(evictedKey);
key3 = keyTable[index3];
if (key3 == EMPTY) {
keyTable[index3] = evictedKey;
valueTable[index3] = evictedValue;
if (size++ >= threshold) resize(capacity << 1);
return;
}
if (++i == pushIterations) break;
insertKey = evictedKey;
insertValue = evictedValue;
} while (true);
putStash(evictedKey, evictedValue);
}
private void putStash (long key, V value) {
if (stashSize == stashCapacity) {
// Too many pushes occurred and the stash is full, increase the table size.
resize(capacity << 1);
putResize(key, value);
return;
}
// Store key in the stash.
int index = capacity + stashSize;
keyTable[index] = key;
valueTable[index] = value;
stashSize++;
size++;
}
public V get (long key) {
if (key == 0) {
if (!hasZeroValue) return null;
return zeroValue;
}
int index = (int)(key & mask);
if (keyTable[index] != key) {
index = hash2(key);
if (keyTable[index] != key) {
index = hash3(key);
if (keyTable[index] != key) return getStash(key, null);
}
}
return valueTable[index];
}
public V get (long key, V defaultValue) {
if (key == 0) {
if (!hasZeroValue) return defaultValue;
return zeroValue;
}
int index = (int)(key & mask);
if (keyTable[index] != key) {
index = hash2(key);
if (keyTable[index] != key) {
index = hash3(key);
if (keyTable[index] != key) return getStash(key, defaultValue);
}
}
return valueTable[index];
}
private V getStash (long key, V defaultValue) {
long[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++)
if (keyTable[i] == key) return valueTable[i];
return defaultValue;
}
public V remove (long key) {
if (key == 0) {
if (!hasZeroValue) return null;
V oldValue = zeroValue;
zeroValue = null;
hasZeroValue = false;
size--;
return oldValue;
}
int index = (int)(key & mask);
if (keyTable[index] == key) {
keyTable[index] = EMPTY;
V oldValue = valueTable[index];
valueTable[index] = null;
size--;
return oldValue;
}
index = hash2(key);
if (keyTable[index] == key) {
keyTable[index] = EMPTY;
V oldValue = valueTable[index];
valueTable[index] = null;
size--;
return oldValue;
}
index = hash3(key);
if (keyTable[index] == key) {
keyTable[index] = EMPTY;
V oldValue = valueTable[index];
valueTable[index] = null;
size--;
return oldValue;
}
return removeStash(key);
}
V removeStash (long key) {
long[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++) {
if (keyTable[i] == key) {
V oldValue = valueTable[i];
removeStashIndex(i);
size--;
return oldValue;
}
}
return null;
}
void removeStashIndex (int index) {
// If the removed location was not last, move the last tuple to the removed location.
stashSize--;
int lastIndex = capacity + stashSize;
if (index < lastIndex) {
keyTable[index] = keyTable[lastIndex];
valueTable[index] = valueTable[lastIndex];
valueTable[lastIndex] = null;
} else
valueTable[index] = null;
}
/** Returns true if the map is empty. */
public boolean isEmpty () {
return size == 0;
}
/** Reduces the size of the backing arrays to be the specified capacity or less. If the capacity is already less, nothing is
* done. If the map contains more items than the specified capacity, the next highest power of two capacity is used instead. */
public void shrink (int maximumCapacity) {
if (maximumCapacity < 0) throw new IllegalArgumentException("maximumCapacity must be >= 0: " + maximumCapacity);
if (size > maximumCapacity) maximumCapacity = size;
if (capacity <= maximumCapacity) return;
maximumCapacity = MathUtil.nextPowerOfTwo(maximumCapacity);
resize(maximumCapacity);
}
/** Clears the map and reduces the size of the backing arrays to be the specified capacity if they are larger. */
public void clear (int maximumCapacity) {
if (capacity <= maximumCapacity) {
clear();
return;
}
zeroValue = null;
hasZeroValue = false;
size = 0;
resize(maximumCapacity);
}
public void clear () {
if (size == 0) return;
long[] keyTable = this.keyTable;
V[] valueTable = this.valueTable;
for (int i = capacity + stashSize; i-- > 0;) {
keyTable[i] = EMPTY;
valueTable[i] = null;
}
size = 0;
stashSize = 0;
zeroValue = null;
hasZeroValue = false;
}
/** Returns true if the specified value is in the map. Note this traverses the entire map and compares every value, which may
* be an expensive operation. */
public boolean containsValue (Object value, boolean identity) {
V[] valueTable = this.valueTable;
if (value == null) {
if (hasZeroValue && zeroValue == null) return true;
long[] keyTable = this.keyTable;
for (int i = capacity + stashSize; i-- > 0;)
if (keyTable[i] != EMPTY && valueTable[i] == null) return true;
} else if (identity) {
if (value == zeroValue) return true;
for (int i = capacity + stashSize; i-- > 0;)
if (valueTable[i] == value) return true;
} else {
if (hasZeroValue && value.equals(zeroValue)) return true;
for (int i = capacity + stashSize; i-- > 0;)
if (value.equals(valueTable[i])) return true;
}
return false;
}
public boolean containsKey (long key) {
if (key == 0) return hasZeroValue;
int index = (int)(key & mask);
if (keyTable[index] != key) {
index = hash2(key);
if (keyTable[index] != key) {
index = hash3(key);
if (keyTable[index] != key) return containsKeyStash(key);
}
}
return true;
}
private boolean containsKeyStash (long key) {
long[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++)
if (keyTable[i] == key) return true;
return false;
}
/** Returns the key for the specified value, or <tt>notFound</tt> if it is not in the map. Note this traverses the entire map
* and compares every value, which may be an expensive operation.
* @param identity If true, uses == to compare the specified value with values in the map. If false, uses
* {@link #equals(Object)}. */
public long findKey (Object value, boolean identity, long notFound) {
V[] valueTable = this.valueTable;
if (value == null) {
if (hasZeroValue && zeroValue == null) return 0;
long[] keyTable = this.keyTable;
for (int i = capacity + stashSize; i-- > 0;)
if (keyTable[i] != EMPTY && valueTable[i] == null) return keyTable[i];
} else if (identity) {
if (value == zeroValue) return 0;
for (int i = capacity + stashSize; i-- > 0;)
if (valueTable[i] == value) return keyTable[i];
} else {
if (hasZeroValue && value.equals(zeroValue)) return 0;
for (int i = capacity + stashSize; i-- > 0;)
if (value.equals(valueTable[i])) return keyTable[i];
}
return notFound;
}
/** Increases the size of the backing array to accommodate the specified number of additional items. Useful before adding many
* items to avoid multiple backing array resizes. */
public void ensureCapacity (int additionalCapacity) {
if (additionalCapacity < 0) throw new IllegalArgumentException("additionalCapacity must be >= 0: " + additionalCapacity);
int sizeNeeded = size + additionalCapacity;
if (sizeNeeded >= threshold) resize(MathUtil.nextPowerOfTwo((int)Math.ceil(sizeNeeded / loadFactor)));
}
private void resize (int newSize) {
int oldEndIndex = capacity + stashSize;
capacity = newSize;
threshold = (int)(newSize * loadFactor);
mask = newSize - 1;
hashShift = 63 - Long.numberOfTrailingZeros(newSize);
stashCapacity = Math.max(3, (int)Math.ceil(Math.log(newSize)) * 2);
pushIterations = Math.max(Math.min(newSize, 8), (int)Math.sqrt(newSize) / 8);
long[] oldKeyTable = keyTable;
V[] oldValueTable = valueTable;
keyTable = new long[newSize + stashCapacity];
valueTable = (V[])new Object[newSize + stashCapacity];
int oldSize = size;
size = hasZeroValue ? 1 : 0;
stashSize = 0;
if (oldSize > 0) {
for (int i = 0; i < oldEndIndex; i++) {
long key = oldKeyTable[i];
if (key != EMPTY) putResize(key, oldValueTable[i]);
}
}
}
private int hash2 (long h) {
h *= PRIME2;
return (int)((h ^ h >>> hashShift) & mask);
}
private int hash3 (long h) {
h *= PRIME3;
return (int)((h ^ h >>> hashShift) & mask);
}
@Override
public int hashCode () {
int h = 0;
if (hasZeroValue && zeroValue != null) {
h += zeroValue.hashCode();
}
long[] keyTable = this.keyTable;
V[] valueTable = this.valueTable;
for (int i = 0, n = capacity + stashSize; i < n; i++) {
long key = keyTable[i];
if (key != EMPTY) {
h += (int)(key ^ (key >>> 32)) * 31;
V value = valueTable[i];
if (value != null) {
h += value.hashCode();
}
}
}
return h;
}
@Override
public boolean equals (Object obj) {
if (obj == this) return true;
if (!(obj instanceof LongMap)) return false;
LongMap<V> other = (LongMap)obj;
if (other.size != size) return false;
if (other.hasZeroValue != hasZeroValue) return false;
if (hasZeroValue) {
if (other.zeroValue == null) {
if (zeroValue != null) return false;
} else {
if (!other.zeroValue.equals(zeroValue)) return false;
}
}
long[] keyTable = this.keyTable;
V[] valueTable = this.valueTable;
for (int i = 0, n = capacity + stashSize; i < n; i++) {
long key = keyTable[i];
if (key != EMPTY) {
V value = valueTable[i];
if (value == null) {
if (!other.containsKey(key) || other.get(key) != null) return false;
} else {
if (!value.equals(other.get(key))) return false;
}
}
}
return true;
}
@Override
public String toString () {
if (size == 0) return "[]";
StringBuilder buffer = new StringBuilder(32);
buffer.append('[');
long[] keyTable = this.keyTable;
V[] valueTable = this.valueTable;
int i = keyTable.length;
while (i-- > 0) {
long key = keyTable[i];
if (key == EMPTY) continue;
buffer.append(key);
buffer.append('=');
buffer.append(valueTable[i]);
break;
}
while (i-- > 0) {
long key = keyTable[i];
if (key == EMPTY) continue;
buffer.append(", ");
buffer.append(key);
buffer.append('=');
buffer.append(valueTable[i]);
}
buffer.append(']');
return buffer.toString();
}
@Override
public Iterator<Entry<V>> iterator () {
return entries();
}
/** Returns an iterator for the entries in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */
public Entries<V> entries () {
if (entries1 == null) {
entries1 = new Entries(this);
entries2 = new Entries(this);
}
if (!entries1.valid) {
entries1.reset();
entries1.valid = true;
entries2.valid = false;
return entries1;
}
entries2.reset();
entries2.valid = true;
entries1.valid = false;
return entries2;
}
/** Returns an iterator for the values in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */
public Values<V> values () {
if (values1 == null) {
values1 = new Values(this);
values2 = new Values(this);
}
if (!values1.valid) {
values1.reset();
values1.valid = true;
values2.valid = false;
return values1;
}
values2.reset();
values2.valid = true;
values1.valid = false;
return values2;
}
/** Returns an iterator for the keys in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */
public Keys keys () {
if (keys1 == null) {
keys1 = new Keys(this);
keys2 = new Keys(this);
}
if (!keys1.valid) {
keys1.reset();
keys1.valid = true;
keys2.valid = false;
return keys1;
}
keys2.reset();
keys2.valid = true;
keys1.valid = false;
return keys2;
}
static public class Entry<V> {
public long key;
public V value;
@Override
public String toString () {
return key + "=" + value;
}
}
static private class MapIterator<V> {
static final int INDEX_ILLEGAL = -2;
static final int INDEX_ZERO = -1;
public boolean hasNext;
final LongMap<V> map;
int nextIndex, currentIndex;
boolean valid = true;
public MapIterator (LongMap<V> map) {
this.map = map;
reset();
}
public void reset () {
currentIndex = INDEX_ILLEGAL;
nextIndex = INDEX_ZERO;
if (map.hasZeroValue)
hasNext = true;
else
findNextIndex();
}
void findNextIndex () {
hasNext = false;
long[] keyTable = map.keyTable;
for (int n = map.capacity + map.stashSize; ++nextIndex < n;) {
if (keyTable[nextIndex] != EMPTY) {
hasNext = true;
break;
}
}
}
public void remove () {
if (currentIndex == INDEX_ZERO && map.hasZeroValue) {
map.zeroValue = null;
map.hasZeroValue = false;
} else if (currentIndex < 0) {
throw new IllegalStateException("next must be called before remove.");
} else if (currentIndex >= map.capacity) {
map.removeStashIndex(currentIndex);
nextIndex = currentIndex - 1;
findNextIndex();
} else {
map.keyTable[currentIndex] = EMPTY;
map.valueTable[currentIndex] = null;
}
currentIndex = INDEX_ILLEGAL;
map.size--;
}
}
@SuppressWarnings("NullableProblems")
static public class Entries<V> extends MapIterator<V> implements Iterable<Entry<V>>, Iterator<Entry<V>> {
private Entry<V> entry = new Entry();
public Entries (LongMap map) {
super(map);
}
/** Note the same entry instance is returned each time this method is called. */
@Override
public Entry<V> next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
long[] keyTable = map.keyTable;
if (nextIndex == INDEX_ZERO) {
entry.key = 0;
entry.value = map.zeroValue;
} else {
entry.key = keyTable[nextIndex];
entry.value = map.valueTable[nextIndex];
}
currentIndex = nextIndex;
findNextIndex();
return entry;
}
@Override
public boolean hasNext () {
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
return hasNext;
}
@Override
public Iterator<Entry<V>> iterator () {
return this;
}
@Override
public void remove () {
super.remove();
}
}
@SuppressWarnings("rawtypes")
static public class Values<V> extends MapIterator<V> implements Iterable<V>, Iterator<V> {
public Values (LongMap<V> map) {
super(map);
}
@Override
public boolean hasNext () {
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
return hasNext;
}
@Override
public V next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
V value;
if (nextIndex == INDEX_ZERO)
value = map.zeroValue;
else
value = map.valueTable[nextIndex];
currentIndex = nextIndex;
findNextIndex();
return value;
}
@Override
public Iterator<V> iterator () {
return this;
}
/** Returns a new array containing the remaining values. */
public Array<V> toArray () {
Array array = new Array(true, map.size);
while (hasNext)
array.add(next());
return array;
}
@Override
public void remove () {
super.remove();
}
}
@SuppressWarnings("rawtypes")
static public class Keys extends MapIterator {
public Keys (LongMap map) {
super(map);
}
public long next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
long key = nextIndex == INDEX_ZERO ? 0 : map.keyTable[nextIndex];
currentIndex = nextIndex;
findNextIndex();
return key;
}
/** Returns a new array containing the remaining values. */
public LongArray toArray () {
LongArray array = new LongArray(true, map.size);
while (hasNext)
array.add(next());
return array;
}
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2010 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.collections;
import java.util.Random;
public
class MathUtil {
public static final Random random = new Random();
/** Returns a random number between 0 (inclusive) and the specified value (inclusive). */
static public int random (int range) {
return random.nextInt(range + 1);
}
/** Returns a random number between start (inclusive) and end (inclusive). */
static public int random (int start, int end) {
return start + random.nextInt(end - start + 1);
}
/**
* Returns the next power of two. Returns the specified value if the value is already a power of two.
*/
public static
int nextPowerOfTwo(int value) {
return 1 << (32 - Integer.numberOfLeadingZeros(value - 1));
}
}

View File

@ -0,0 +1,795 @@
/*******************************************************************************
* Copyright 2011 LibGDX.
* Mario Zechner <badlogicgames@gmail.com>
* Nathan Sweet <nathan.sweet@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package dorkbox.collections;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Random;
/** An unordered map where the values are floats. This implementation is a cuckoo hash map using 3 hashes, random walking, and a
* small stash for problematic keys. Null keys are not allowed. No allocation is done except when growing the table size. <br>
* <br>
* This map performs very fast get, containsKey, and remove (typically O(1), worst case O(log(n))). Put may be a bit slower,
* depending on hash collisions. Load factors greater than 0.91 greatly increase the chances the map will have to rehash to the
* next higher POT size.
* @author Nathan Sweet */
@SuppressWarnings({"unchecked", "NullableProblems", "rawtypes"})
public class ObjectFloatMap<K> implements Iterable<ObjectFloatMap.Entry<K>> {
private static final int PRIME1 = 0xbe1f14b1;
private static final int PRIME2 = 0xb4b82e39;
private static final int PRIME3 = 0xced1c241;
public int size;
K[] keyTable;
float[] valueTable;
int capacity, stashSize;
private float loadFactor;
private int hashShift, mask, threshold;
private int stashCapacity;
private int pushIterations;
private Entries entries1, entries2;
private Values values1, values2;
private Keys keys1, keys2;
/** Creates a new map with an initial capacity of 51 and a load factor of 0.8. */
public ObjectFloatMap () {
this(51, 0.8f);
}
/** Creates a new map with a load factor of 0.8.
* @param initialCapacity If not a power of two, it is increased to the next nearest power of two. */
public ObjectFloatMap (int initialCapacity) {
this(initialCapacity, 0.8f);
}
/** Creates a new map with the specified initial capacity and load factor. This map will hold initialCapacity items before
* growing the backing table.
* @param initialCapacity If not a power of two, it is increased to the next nearest power of two. */
@SuppressWarnings("unchecked")
public ObjectFloatMap (int initialCapacity, float loadFactor) {
if (initialCapacity < 0) throw new IllegalArgumentException("initialCapacity must be >= 0: " + initialCapacity);
initialCapacity = MathUtil.nextPowerOfTwo((int)Math.ceil(initialCapacity / loadFactor));
if (initialCapacity > 1 << 30) throw new IllegalArgumentException("initialCapacity is too large: " + initialCapacity);
capacity = initialCapacity;
if (loadFactor <= 0) throw new IllegalArgumentException("loadFactor must be > 0: " + loadFactor);
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
mask = capacity - 1;
hashShift = 31 - Integer.numberOfTrailingZeros(capacity);
stashCapacity = Math.max(3, (int)Math.ceil(Math.log(capacity)) * 2);
pushIterations = Math.max(Math.min(capacity, 8), (int)Math.sqrt(capacity) / 8);
keyTable = (K[])new Object[capacity + stashCapacity];
valueTable = new float[keyTable.length];
}
/** Creates a new map identical to the specified map. */
public ObjectFloatMap (ObjectFloatMap<? extends K> map) {
this((int)Math.floor(map.capacity * map.loadFactor), map.loadFactor);
stashSize = map.stashSize;
System.arraycopy(map.keyTable, 0, keyTable, 0, map.keyTable.length);
System.arraycopy(map.valueTable, 0, valueTable, 0, map.valueTable.length);
size = map.size;
}
public void put (K key, float value) {
if (key == null) throw new IllegalArgumentException("key cannot be null.");
K[] keyTable = this.keyTable;
// Check for existing keys.
int hashCode = key.hashCode();
int index1 = hashCode & mask;
K key1 = keyTable[index1];
if (key.equals(key1)) {
valueTable[index1] = value;
return;
}
int index2 = hash2(hashCode);
K key2 = keyTable[index2];
if (key.equals(key2)) {
valueTable[index2] = value;
return;
}
int index3 = hash3(hashCode);
K key3 = keyTable[index3];
if (key.equals(key3)) {
valueTable[index3] = value;
return;
}
// Update key in the stash.
for (int i = capacity, n = i + stashSize; i < n; i++) {
if (key.equals(keyTable[i])) {
valueTable[i] = value;
return;
}
}
// Check for empty buckets.
if (key1 == null) {
keyTable[index1] = key;
valueTable[index1] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
if (key2 == null) {
keyTable[index2] = key;
valueTable[index2] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
if (key3 == null) {
keyTable[index3] = key;
valueTable[index3] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
push(key, value, index1, key1, index2, key2, index3, key3);
}
public void putAll (ObjectFloatMap<? extends K> map) {
for (Entry<? extends K> entry : map.entries())
put(entry.key, entry.value);
}
/** Skips checks for existing keys. */
private void putResize (K key, float value) {
// Check for empty buckets.
int hashCode = key.hashCode();
int index1 = hashCode & mask;
K key1 = keyTable[index1];
if (key1 == null) {
keyTable[index1] = key;
valueTable[index1] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
int index2 = hash2(hashCode);
K key2 = keyTable[index2];
if (key2 == null) {
keyTable[index2] = key;
valueTable[index2] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
int index3 = hash3(hashCode);
K key3 = keyTable[index3];
if (key3 == null) {
keyTable[index3] = key;
valueTable[index3] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
push(key, value, index1, key1, index2, key2, index3, key3);
}
private void push (K insertKey, float insertValue, int index1, K key1, int index2, K key2, int index3, K key3) {
K[] keyTable = this.keyTable;
float[] valueTable = this.valueTable;
int mask = this.mask;
// Push keys until an empty bucket is found.
K evictedKey;
float evictedValue;
int i = 0, pushIterations = this.pushIterations;
do {
// Replace the key and value for one of the hashes.
switch (MathUtil.random(2)) {
case 0:
evictedKey = key1;
evictedValue = valueTable[index1];
keyTable[index1] = insertKey;
valueTable[index1] = insertValue;
break;
case 1:
evictedKey = key2;
evictedValue = valueTable[index2];
keyTable[index2] = insertKey;
valueTable[index2] = insertValue;
break;
default:
evictedKey = key3;
evictedValue = valueTable[index3];
keyTable[index3] = insertKey;
valueTable[index3] = insertValue;
break;
}
// If the evicted key hashes to an empty bucket, put it there and stop.
int hashCode = evictedKey.hashCode();
index1 = hashCode & mask;
key1 = keyTable[index1];
if (key1 == null) {
keyTable[index1] = evictedKey;
valueTable[index1] = evictedValue;
if (size++ >= threshold) resize(capacity << 1);
return;
}
index2 = hash2(hashCode);
key2 = keyTable[index2];
if (key2 == null) {
keyTable[index2] = evictedKey;
valueTable[index2] = evictedValue;
if (size++ >= threshold) resize(capacity << 1);
return;
}
index3 = hash3(hashCode);
key3 = keyTable[index3];
if (key3 == null) {
keyTable[index3] = evictedKey;
valueTable[index3] = evictedValue;
if (size++ >= threshold) resize(capacity << 1);
return;
}
if (++i == pushIterations) break;
insertKey = evictedKey;
insertValue = evictedValue;
} while (true);
putStash(evictedKey, evictedValue);
}
private void putStash (K key, float value) {
if (stashSize == stashCapacity) {
// Too many pushes occurred and the stash is full, increase the table size.
resize(capacity << 1);
putResize(key, value);
return;
}
// Store key in the stash.
int index = capacity + stashSize;
keyTable[index] = key;
valueTable[index] = value;
stashSize++;
size++;
}
/** @param defaultValue Returned if the key was not associated with a value. */
public float get (K key, float defaultValue) {
int hashCode = key.hashCode();
int index = hashCode & mask;
if (!key.equals(keyTable[index])) {
index = hash2(hashCode);
if (!key.equals(keyTable[index])) {
index = hash3(hashCode);
if (!key.equals(keyTable[index])) return getStash(key, defaultValue);
}
}
return valueTable[index];
}
private float getStash (K key, float defaultValue) {
K[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++)
if (key.equals(keyTable[i])) return valueTable[i];
return defaultValue;
}
/** Returns the key's current value and increments the stored value. If the key is not in the map, defaultValue + increment is
* put into the map. */
public float getAndIncrement (K key, float defaultValue, float increment) {
int hashCode = key.hashCode();
int index = hashCode & mask;
if (!key.equals(keyTable[index])) {
index = hash2(hashCode);
if (!key.equals(keyTable[index])) {
index = hash3(hashCode);
if (!key.equals(keyTable[index])) return getAndIncrementStash(key, defaultValue, increment);
}
}
float value = valueTable[index];
valueTable[index] = value + increment;
return value;
}
private float getAndIncrementStash (K key, float defaultValue, float increment) {
K[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++)
if (key.equals(keyTable[i])) {
float value = valueTable[i];
valueTable[i] = value + increment;
return value;
}
put(key, defaultValue + increment);
return defaultValue;
}
public float remove (K key, float defaultValue) {
int hashCode = key.hashCode();
int index = hashCode & mask;
if (key.equals(keyTable[index])) {
keyTable[index] = null;
float oldValue = valueTable[index];
size--;
return oldValue;
}
index = hash2(hashCode);
if (key.equals(keyTable[index])) {
keyTable[index] = null;
float oldValue = valueTable[index];
size--;
return oldValue;
}
index = hash3(hashCode);
if (key.equals(keyTable[index])) {
keyTable[index] = null;
float oldValue = valueTable[index];
size--;
return oldValue;
}
return removeStash(key, defaultValue);
}
float removeStash (K key, float defaultValue) {
K[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++) {
if (key.equals(keyTable[i])) {
float oldValue = valueTable[i];
removeStashIndex(i);
size--;
return oldValue;
}
}
return defaultValue;
}
void removeStashIndex (int index) {
// If the removed location was not last, move the last tuple to the removed location.
stashSize--;
int lastIndex = capacity + stashSize;
if (index < lastIndex) {
keyTable[index] = keyTable[lastIndex];
valueTable[index] = valueTable[lastIndex];
keyTable[lastIndex] = null;
}
}
/** Returns true if the map is empty. */
public boolean isEmpty () {
return size == 0;
}
/** Reduces the size of the backing arrays to be the specified capacity or less. If the capacity is already less, nothing is
* done. If the map contains more items than the specified capacity, the next highest power of two capacity is used instead. */
public void shrink (int maximumCapacity) {
if (maximumCapacity < 0) throw new IllegalArgumentException("maximumCapacity must be >= 0: " + maximumCapacity);
if (size > maximumCapacity) maximumCapacity = size;
if (capacity <= maximumCapacity) return;
maximumCapacity = MathUtil.nextPowerOfTwo(maximumCapacity);
resize(maximumCapacity);
}
/** Clears the map and reduces the size of the backing arrays to be the specified capacity if they are larger. */
public void clear (int maximumCapacity) {
if (capacity <= maximumCapacity) {
clear();
return;
}
size = 0;
resize(maximumCapacity);
}
public void clear () {
if (size == 0) return;
K[] keyTable = this.keyTable;
for (int i = capacity + stashSize; i-- > 0;)
keyTable[i] = null;
size = 0;
stashSize = 0;
}
/** Returns true if the specified value is in the map. Note this traverses the entire map and compares every value, which may be
* an expensive operation. */
public boolean containsValue (float value) {
K[] keyTable = this.keyTable;
float[] valueTable = this.valueTable;
for (int i = capacity + stashSize; i-- > 0;)
if (keyTable[i] != null && valueTable[i] == value) return true;
return false;
}
public boolean containsKey (K key) {
int hashCode = key.hashCode();
int index = hashCode & mask;
if (!key.equals(keyTable[index])) {
index = hash2(hashCode);
if (!key.equals(keyTable[index])) {
index = hash3(hashCode);
if (!key.equals(keyTable[index])) return containsKeyStash(key);
}
}
return true;
}
private boolean containsKeyStash (K key) {
K[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++)
if (key.equals(keyTable[i])) return true;
return false;
}
/** Returns the key for the specified value, or null if it is not in the map. Note this traverses the entire map and compares
* every value, which may be an expensive operation. */
public K findKey (float value) {
K[] keyTable = this.keyTable;
float[] valueTable = this.valueTable;
for (int i = capacity + stashSize; i-- > 0;)
if (keyTable[i] != null && valueTable[i] == value) return keyTable[i];
return null;
}
/** Increases the size of the backing array to accommodate the specified number of additional items. Useful before adding many
* items to avoid multiple backing array resizes. */
public void ensureCapacity (int additionalCapacity) {
if (additionalCapacity < 0) throw new IllegalArgumentException("additionalCapacity must be >= 0: " + additionalCapacity);
int sizeNeeded = size + additionalCapacity;
if (sizeNeeded >= threshold) resize(MathUtil.nextPowerOfTwo((int)Math.ceil(sizeNeeded / loadFactor)));
}
private void resize (int newSize) {
int oldEndIndex = capacity + stashSize;
capacity = newSize;
threshold = (int)(newSize * loadFactor);
mask = newSize - 1;
hashShift = 31 - Integer.numberOfTrailingZeros(newSize);
stashCapacity = Math.max(3, (int)Math.ceil(Math.log(newSize)) * 2);
pushIterations = Math.max(Math.min(newSize, 8), (int)Math.sqrt(newSize) / 8);
K[] oldKeyTable = keyTable;
float[] oldValueTable = valueTable;
keyTable = (K[])new Object[newSize + stashCapacity];
valueTable = new float[newSize + stashCapacity];
int oldSize = size;
size = 0;
stashSize = 0;
if (oldSize > 0) {
for (int i = 0; i < oldEndIndex; i++) {
K key = oldKeyTable[i];
if (key != null) putResize(key, oldValueTable[i]);
}
}
}
private int hash2 (int h) {
h *= PRIME2;
return (h ^ h >>> hashShift) & mask;
}
private int hash3 (int h) {
h *= PRIME3;
return (h ^ h >>> hashShift) & mask;
}
@Override
public int hashCode () {
int h = 0;
K[] keyTable = this.keyTable;
float[] valueTable = this.valueTable;
for (int i = 0, n = capacity + stashSize; i < n; i++) {
K key = keyTable[i];
if (key != null) {
h += key.hashCode() * 31;
float value = valueTable[i];
h += Float.floatToIntBits(value);
}
}
return h;
}
@Override
public boolean equals (Object obj) {
if (obj == this) return true;
if (!(obj instanceof ObjectFloatMap)) return false;
ObjectFloatMap<K> other = (ObjectFloatMap) obj;
if (other.size != size) return false;
K[] keyTable = this.keyTable;
float[] valueTable = this.valueTable;
for (int i = 0, n = capacity + stashSize; i < n; i++) {
K key = keyTable[i];
if (key != null) {
float otherValue = other.get(key, 0f);
if (otherValue == 0f && !other.containsKey(key)) return false;
float value = valueTable[i];
if (otherValue != value) return false;
}
}
return true;
}
@Override
public String toString () {
if (size == 0) return "{}";
StringBuilder buffer = new StringBuilder(32);
buffer.append('{');
K[] keyTable = this.keyTable;
float[] valueTable = this.valueTable;
int i = keyTable.length;
while (i-- > 0) {
K key = keyTable[i];
if (key == null) continue;
buffer.append(key);
buffer.append('=');
buffer.append(valueTable[i]);
break;
}
while (i-- > 0) {
K key = keyTable[i];
if (key == null) continue;
buffer.append(", ");
buffer.append(key);
buffer.append('=');
buffer.append(valueTable[i]);
}
buffer.append('}');
return buffer.toString();
}
@Override
public Entries<K> iterator () {
return entries();
}
/** Returns an iterator for the entries in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */
public Entries<K> entries () {
if (entries1 == null) {
entries1 = new Entries(this);
entries2 = new Entries(this);
}
if (!entries1.valid) {
entries1.reset();
entries1.valid = true;
entries2.valid = false;
return entries1;
}
entries2.reset();
entries2.valid = true;
entries1.valid = false;
return entries2;
}
/** Returns an iterator for the values in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */
public Values values () {
if (values1 == null) {
values1 = new Values(this);
values2 = new Values(this);
}
if (!values1.valid) {
values1.reset();
values1.valid = true;
values2.valid = false;
return values1;
}
values2.reset();
values2.valid = true;
values1.valid = false;
return values2;
}
/** Returns an iterator for the keys in the map. Remove is supported. Note that the same iterator instance is returned each time
* this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */
public Keys<K> keys () {
if (keys1 == null) {
keys1 = new Keys(this);
keys2 = new Keys(this);
}
if (!keys1.valid) {
keys1.reset();
keys1.valid = true;
keys2.valid = false;
return keys1;
}
keys2.reset();
keys2.valid = true;
keys1.valid = false;
return keys2;
}
static public class Entry<K> {
public K key;
public float value;
@Override
public String toString () {
return key + "=" + value;
}
}
static private class MapIterator<K> {
public boolean hasNext;
final ObjectFloatMap<K> map;
int nextIndex, currentIndex;
boolean valid = true;
public MapIterator (ObjectFloatMap<K> map) {
this.map = map;
reset();
}
public void reset () {
currentIndex = -1;
nextIndex = -1;
findNextIndex();
}
void findNextIndex () {
hasNext = false;
K[] keyTable = map.keyTable;
for (int n = map.capacity + map.stashSize; ++nextIndex < n;) {
if (keyTable[nextIndex] != null) {
hasNext = true;
break;
}
}
}
public void remove () {
if (currentIndex < 0) throw new IllegalStateException("next must be called before remove.");
if (currentIndex >= map.capacity) {
map.removeStashIndex(currentIndex);
nextIndex = currentIndex - 1;
findNextIndex();
} else {
map.keyTable[currentIndex] = null;
}
currentIndex = -1;
map.size--;
}
}
static public class Entries<K> extends MapIterator<K> implements Iterable<Entry<K>>, Iterator<Entry<K>> {
private Entry<K> entry = new Entry();
public Entries (ObjectFloatMap<K> map) {
super(map);
}
/** Note the same entry instance is returned each time this method is called. */
@Override
public Entry<K> next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
K[] keyTable = map.keyTable;
entry.key = keyTable[nextIndex];
entry.value = map.valueTable[nextIndex];
currentIndex = nextIndex;
findNextIndex();
return entry;
}
@Override
public boolean hasNext () {
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
return hasNext;
}
@Override
public Entries<K> iterator () {
return this;
}
@Override
public void remove () {
super.remove();
}
}
static public class Values extends MapIterator<Object> {
public Values (ObjectFloatMap<?> map) {
super((ObjectFloatMap<Object>)map);
}
public boolean hasNext () {
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
return hasNext;
}
public float next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
float value = map.valueTable[nextIndex];
currentIndex = nextIndex;
findNextIndex();
return value;
}
/** Returns a new array containing the remaining values. */
public FloatArray toArray () {
FloatArray array = new FloatArray(true, map.size);
while (hasNext)
array.add(next());
return array;
}
}
@SuppressWarnings("unchecked")
static public class Keys<K> extends MapIterator<K> implements Iterable<K>, Iterator<K> {
public Keys (ObjectFloatMap<K> map) {
super((ObjectFloatMap<K>)map);
}
@Override
public boolean hasNext () {
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
return hasNext;
}
@Override
public K next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
K key = map.keyTable[nextIndex];
currentIndex = nextIndex;
findNextIndex();
return key;
}
@Override
public Keys<K> iterator () {
return this;
}
/** Returns a new array containing the remaining keys. */
public Array<K> toArray () {
Array array = new Array(true, map.size);
while (hasNext)
array.add(next());
return array;
}
/** Adds the remaining keys to the array. */
public Array<K> toArray (Array<K> array) {
while (hasNext)
array.add(next());
return array;
}
@Override
public void remove () {
super.remove();
}
}
}

View File

@ -0,0 +1,794 @@
/*******************************************************************************
* Copyright 2011 LibGDX.
* Mario Zechner <badlogicgames@gmail.com>
* Nathan Sweet <nathan.sweet@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package dorkbox.collections;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Random;
/** An unordered map where the values are ints. This implementation is a cuckoo hash map using 3 hashes, random walking, and a
* small stash for problematic keys. Null keys are not allowed. No allocation is done except when growing the table size. <br>
* <br>
* This map performs very fast get, containsKey, and remove (typically O(1), worst case O(log(n))). Put may be a bit slower,
* depending on hash collisions. Load factors greater than 0.91 greatly increase the chances the map will have to rehash to the
* next higher POT size.
* @author Nathan Sweet */
@SuppressWarnings({"unchecked", "NullableProblems"})
public class ObjectIntMap<K> implements Iterable<ObjectIntMap.Entry<K>> {
private static final int PRIME1 = 0xbe1f14b1;
private static final int PRIME2 = 0xb4b82e39;
private static final int PRIME3 = 0xced1c241;
public int size;
K[] keyTable;
int[] valueTable;
int capacity, stashSize;
private float loadFactor;
private int hashShift, mask, threshold;
private int stashCapacity;
private int pushIterations;
private Entries entries1, entries2;
private Values values1, values2;
private Keys keys1, keys2;
/** Creates a new map with an initial capacity of 51 and a load factor of 0.8. */
public ObjectIntMap () {
this(51, 0.8f);
}
/** Creates a new map with a load factor of 0.8.
* @param initialCapacity If not a power of two, it is increased to the next nearest power of two. */
public ObjectIntMap (int initialCapacity) {
this(initialCapacity, 0.8f);
}
/** Creates a new map with the specified initial capacity and load factor. This map will hold initialCapacity items before
* growing the backing table.
* @param initialCapacity If not a power of two, it is increased to the next nearest power of two. */
public ObjectIntMap (int initialCapacity, float loadFactor) {
if (initialCapacity < 0) throw new IllegalArgumentException("initialCapacity must be >= 0: " + initialCapacity);
initialCapacity = MathUtil.nextPowerOfTwo((int)Math.ceil(initialCapacity / loadFactor));
if (initialCapacity > 1 << 30) throw new IllegalArgumentException("initialCapacity is too large: " + initialCapacity);
capacity = initialCapacity;
if (loadFactor <= 0) throw new IllegalArgumentException("loadFactor must be > 0: " + loadFactor);
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
mask = capacity - 1;
hashShift = 31 - Integer.numberOfTrailingZeros(capacity);
stashCapacity = Math.max(3, (int)Math.ceil(Math.log(capacity)) * 2);
pushIterations = Math.max(Math.min(capacity, 8), (int)Math.sqrt(capacity) / 8);
keyTable = (K[])new Object[capacity + stashCapacity];
valueTable = new int[keyTable.length];
}
/** Creates a new map identical to the specified map. */
public ObjectIntMap (ObjectIntMap<? extends K> map) {
this((int)Math.floor(map.capacity * map.loadFactor), map.loadFactor);
stashSize = map.stashSize;
System.arraycopy(map.keyTable, 0, keyTable, 0, map.keyTable.length);
System.arraycopy(map.valueTable, 0, valueTable, 0, map.valueTable.length);
size = map.size;
}
public void put (K key, int value) {
if (key == null) throw new IllegalArgumentException("key cannot be null.");
K[] keyTable = this.keyTable;
// Check for existing keys.
int hashCode = key.hashCode();
int index1 = hashCode & mask;
K key1 = keyTable[index1];
if (key.equals(key1)) {
valueTable[index1] = value;
return;
}
int index2 = hash2(hashCode);
K key2 = keyTable[index2];
if (key.equals(key2)) {
valueTable[index2] = value;
return;
}
int index3 = hash3(hashCode);
K key3 = keyTable[index3];
if (key.equals(key3)) {
valueTable[index3] = value;
return;
}
// Update key in the stash.
for (int i = capacity, n = i + stashSize; i < n; i++) {
if (key.equals(keyTable[i])) {
valueTable[i] = value;
return;
}
}
// Check for empty buckets.
if (key1 == null) {
keyTable[index1] = key;
valueTable[index1] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
if (key2 == null) {
keyTable[index2] = key;
valueTable[index2] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
if (key3 == null) {
keyTable[index3] = key;
valueTable[index3] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
push(key, value, index1, key1, index2, key2, index3, key3);
}
public void putAll (ObjectIntMap<? extends K> map) {
for (Entry<? extends K> entry : map.entries())
put(entry.key, entry.value);
}
/** Skips checks for existing keys. */
private void putResize (K key, int value) {
// Check for empty buckets.
int hashCode = key.hashCode();
int index1 = hashCode & mask;
K key1 = keyTable[index1];
if (key1 == null) {
keyTable[index1] = key;
valueTable[index1] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
int index2 = hash2(hashCode);
K key2 = keyTable[index2];
if (key2 == null) {
keyTable[index2] = key;
valueTable[index2] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
int index3 = hash3(hashCode);
K key3 = keyTable[index3];
if (key3 == null) {
keyTable[index3] = key;
valueTable[index3] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
push(key, value, index1, key1, index2, key2, index3, key3);
}
private void push (K insertKey, int insertValue, int index1, K key1, int index2, K key2, int index3, K key3) {
K[] keyTable = this.keyTable;
int[] valueTable = this.valueTable;
int mask = this.mask;
// Push keys until an empty bucket is found.
K evictedKey;
int evictedValue;
int i = 0, pushIterations = this.pushIterations;
do {
// Replace the key and value for one of the hashes.
switch (MathUtil.random(2)) {
case 0:
evictedKey = key1;
evictedValue = valueTable[index1];
keyTable[index1] = insertKey;
valueTable[index1] = insertValue;
break;
case 1:
evictedKey = key2;
evictedValue = valueTable[index2];
keyTable[index2] = insertKey;
valueTable[index2] = insertValue;
break;
default:
evictedKey = key3;
evictedValue = valueTable[index3];
keyTable[index3] = insertKey;
valueTable[index3] = insertValue;
break;
}
// If the evicted key hashes to an empty bucket, put it there and stop.
int hashCode = evictedKey.hashCode();
index1 = hashCode & mask;
key1 = keyTable[index1];
if (key1 == null) {
keyTable[index1] = evictedKey;
valueTable[index1] = evictedValue;
if (size++ >= threshold) resize(capacity << 1);
return;
}
index2 = hash2(hashCode);
key2 = keyTable[index2];
if (key2 == null) {
keyTable[index2] = evictedKey;
valueTable[index2] = evictedValue;
if (size++ >= threshold) resize(capacity << 1);
return;
}
index3 = hash3(hashCode);
key3 = keyTable[index3];
if (key3 == null) {
keyTable[index3] = evictedKey;
valueTable[index3] = evictedValue;
if (size++ >= threshold) resize(capacity << 1);
return;
}
if (++i == pushIterations) break;
insertKey = evictedKey;
insertValue = evictedValue;
} while (true);
putStash(evictedKey, evictedValue);
}
private void putStash (K key, int value) {
if (stashSize == stashCapacity) {
// Too many pushes occurred and the stash is full, increase the table size.
resize(capacity << 1);
putResize(key, value);
return;
}
// Store key in the stash.
int index = capacity + stashSize;
keyTable[index] = key;
valueTable[index] = value;
stashSize++;
size++;
}
/** @param defaultValue Returned if the key was not associated with a value. */
public int get (K key, int defaultValue) {
int hashCode = key.hashCode();
int index = hashCode & mask;
if (!key.equals(keyTable[index])) {
index = hash2(hashCode);
if (!key.equals(keyTable[index])) {
index = hash3(hashCode);
if (!key.equals(keyTable[index])) return getStash(key, defaultValue);
}
}
return valueTable[index];
}
private int getStash (K key, int defaultValue) {
K[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++)
if (key.equals(keyTable[i])) return valueTable[i];
return defaultValue;
}
/** Returns the key's current value and increments the stored value. If the key is not in the map, defaultValue + increment is
* put into the map. */
public int getAndIncrement (K key, int defaultValue, int increment) {
int hashCode = key.hashCode();
int index = hashCode & mask;
if (!key.equals(keyTable[index])) {
index = hash2(hashCode);
if (!key.equals(keyTable[index])) {
index = hash3(hashCode);
if (!key.equals(keyTable[index])) return getAndIncrementStash(key, defaultValue, increment);
}
}
int value = valueTable[index];
valueTable[index] = value + increment;
return value;
}
private int getAndIncrementStash (K key, int defaultValue, int increment) {
K[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++)
if (key.equals(keyTable[i])) {
int value = valueTable[i];
valueTable[i] = value + increment;
return value;
}
put(key, defaultValue + increment);
return defaultValue;
}
public int remove (K key, int defaultValue) {
int hashCode = key.hashCode();
int index = hashCode & mask;
if (key.equals(keyTable[index])) {
keyTable[index] = null;
int oldValue = valueTable[index];
size--;
return oldValue;
}
index = hash2(hashCode);
if (key.equals(keyTable[index])) {
keyTable[index] = null;
int oldValue = valueTable[index];
size--;
return oldValue;
}
index = hash3(hashCode);
if (key.equals(keyTable[index])) {
keyTable[index] = null;
int oldValue = valueTable[index];
size--;
return oldValue;
}
return removeStash(key, defaultValue);
}
int removeStash (K key, int defaultValue) {
K[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++) {
if (key.equals(keyTable[i])) {
int oldValue = valueTable[i];
removeStashIndex(i);
size--;
return oldValue;
}
}
return defaultValue;
}
void removeStashIndex (int index) {
// If the removed location was not last, move the last tuple to the removed location.
stashSize--;
int lastIndex = capacity + stashSize;
if (index < lastIndex) {
keyTable[index] = keyTable[lastIndex];
valueTable[index] = valueTable[lastIndex];
keyTable[lastIndex] = null;
}
}
/** Returns true if the map is empty. */
public boolean isEmpty () {
return size == 0;
}
/** Reduces the size of the backing arrays to be the specified capacity or less. If the capacity is already less, nothing is
* done. If the map contains more items than the specified capacity, the next highest power of two capacity is used instead. */
public void shrink (int maximumCapacity) {
if (maximumCapacity < 0) throw new IllegalArgumentException("maximumCapacity must be >= 0: " + maximumCapacity);
if (size > maximumCapacity) maximumCapacity = size;
if (capacity <= maximumCapacity) return;
maximumCapacity = MathUtil.nextPowerOfTwo(maximumCapacity);
resize(maximumCapacity);
}
/** Clears the map and reduces the size of the backing arrays to be the specified capacity if they are larger. */
public void clear (int maximumCapacity) {
if (capacity <= maximumCapacity) {
clear();
return;
}
size = 0;
resize(maximumCapacity);
}
public void clear () {
if (size == 0) return;
K[] keyTable = this.keyTable;
for (int i = capacity + stashSize; i-- > 0;)
keyTable[i] = null;
size = 0;
stashSize = 0;
}
/** Returns true if the specified value is in the map. Note this traverses the entire map and compares every value, which may be
* an expensive operation. */
public boolean containsValue (int value) {
K[] keyTable = this.keyTable;
int[] valueTable = this.valueTable;
for (int i = capacity + stashSize; i-- > 0;)
if (keyTable[i] != null && valueTable[i] == value) return true;
return false;
}
public boolean containsKey (K key) {
int hashCode = key.hashCode();
int index = hashCode & mask;
if (!key.equals(keyTable[index])) {
index = hash2(hashCode);
if (!key.equals(keyTable[index])) {
index = hash3(hashCode);
if (!key.equals(keyTable[index])) return containsKeyStash(key);
}
}
return true;
}
private boolean containsKeyStash (K key) {
K[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++)
if (key.equals(keyTable[i])) return true;
return false;
}
/** Returns the key for the specified value, or null if it is not in the map. Note this traverses the entire map and compares
* every value, which may be an expensive operation. */
public K findKey (int value) {
K[] keyTable = this.keyTable;
int[] valueTable = this.valueTable;
for (int i = capacity + stashSize; i-- > 0;)
if (keyTable[i] != null && valueTable[i] == value) return keyTable[i];
return null;
}
/** Increases the size of the backing array to accommodate the specified number of additional items. Useful before adding many
* items to avoid multiple backing array resizes. */
public void ensureCapacity (int additionalCapacity) {
if (additionalCapacity < 0) throw new IllegalArgumentException("additionalCapacity must be >= 0: " + additionalCapacity);
int sizeNeeded = size + additionalCapacity;
if (sizeNeeded >= threshold) resize(MathUtil.nextPowerOfTwo((int)Math.ceil(sizeNeeded / loadFactor)));
}
private void resize (int newSize) {
int oldEndIndex = capacity + stashSize;
capacity = newSize;
threshold = (int)(newSize * loadFactor);
mask = newSize - 1;
hashShift = 31 - Integer.numberOfTrailingZeros(newSize);
stashCapacity = Math.max(3, (int)Math.ceil(Math.log(newSize)) * 2);
pushIterations = Math.max(Math.min(newSize, 8), (int)Math.sqrt(newSize) / 8);
K[] oldKeyTable = keyTable;
int[] oldValueTable = valueTable;
keyTable = (K[])new Object[newSize + stashCapacity];
valueTable = new int[newSize + stashCapacity];
int oldSize = size;
size = 0;
stashSize = 0;
if (oldSize > 0) {
for (int i = 0; i < oldEndIndex; i++) {
K key = oldKeyTable[i];
if (key != null) putResize(key, oldValueTable[i]);
}
}
}
private int hash2 (int h) {
h *= PRIME2;
return (h ^ h >>> hashShift) & mask;
}
private int hash3 (int h) {
h *= PRIME3;
return (h ^ h >>> hashShift) & mask;
}
@Override
public int hashCode () {
int h = 0;
K[] keyTable = this.keyTable;
int[] valueTable = this.valueTable;
for (int i = 0, n = capacity + stashSize; i < n; i++) {
K key = keyTable[i];
if (key != null) {
h += key.hashCode() * 31;
int value = valueTable[i];
h += value;
}
}
return h;
}
@Override
public boolean equals (Object obj) {
if (obj == this) return true;
if (!(obj instanceof ObjectIntMap)) return false;
ObjectIntMap<K> other = (ObjectIntMap)obj;
if (other.size != size) return false;
K[] keyTable = this.keyTable;
int[] valueTable = this.valueTable;
for (int i = 0, n = capacity + stashSize; i < n; i++) {
K key = keyTable[i];
if (key != null) {
int otherValue = other.get(key, 0);
if (otherValue == 0 && !other.containsKey(key)) return false;
int value = valueTable[i];
if (otherValue != value) return false;
}
}
return true;
}
@Override
public String toString () {
if (size == 0) return "{}";
StringBuilder buffer = new StringBuilder(32);
buffer.append('{');
K[] keyTable = this.keyTable;
int[] valueTable = this.valueTable;
int i = keyTable.length;
while (i-- > 0) {
K key = keyTable[i];
if (key == null) continue;
buffer.append(key);
buffer.append('=');
buffer.append(valueTable[i]);
break;
}
while (i-- > 0) {
K key = keyTable[i];
if (key == null) continue;
buffer.append(", ");
buffer.append(key);
buffer.append('=');
buffer.append(valueTable[i]);
}
buffer.append('}');
return buffer.toString();
}
@Override
public Entries<K> iterator () {
return entries();
}
/** Returns an iterator for the entries in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */
public Entries<K> entries () {
if (entries1 == null) {
entries1 = new Entries(this);
entries2 = new Entries(this);
}
if (!entries1.valid) {
entries1.reset();
entries1.valid = true;
entries2.valid = false;
return entries1;
}
entries2.reset();
entries2.valid = true;
entries1.valid = false;
return entries2;
}
/** Returns an iterator for the values in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */
public Values values () {
if (values1 == null) {
values1 = new Values(this);
values2 = new Values(this);
}
if (!values1.valid) {
values1.reset();
values1.valid = true;
values2.valid = false;
return values1;
}
values2.reset();
values2.valid = true;
values1.valid = false;
return values2;
}
/** Returns an iterator for the keys in the map. Remove is supported. Note that the same iterator instance is returned each time
* this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */
public Keys<K> keys () {
if (keys1 == null) {
keys1 = new Keys(this);
keys2 = new Keys(this);
}
if (!keys1.valid) {
keys1.reset();
keys1.valid = true;
keys2.valid = false;
return keys1;
}
keys2.reset();
keys2.valid = true;
keys1.valid = false;
return keys2;
}
static public class Entry<K> {
public K key;
public int value;
@Override
public String toString () {
return key + "=" + value;
}
}
static private class MapIterator<K> {
public boolean hasNext;
final ObjectIntMap<K> map;
int nextIndex, currentIndex;
boolean valid = true;
public MapIterator (ObjectIntMap<K> map) {
this.map = map;
reset();
}
public void reset () {
currentIndex = -1;
nextIndex = -1;
findNextIndex();
}
void findNextIndex () {
hasNext = false;
K[] keyTable = map.keyTable;
for (int n = map.capacity + map.stashSize; ++nextIndex < n;) {
if (keyTable[nextIndex] != null) {
hasNext = true;
break;
}
}
}
public void remove () {
if (currentIndex < 0) throw new IllegalStateException("next must be called before remove.");
if (currentIndex >= map.capacity) {
map.removeStashIndex(currentIndex);
nextIndex = currentIndex - 1;
findNextIndex();
} else {
map.keyTable[currentIndex] = null;
}
currentIndex = -1;
map.size--;
}
}
static public class Entries<K> extends MapIterator<K> implements Iterable<Entry<K>>, Iterator<Entry<K>> {
private Entry<K> entry = new Entry();
public Entries (ObjectIntMap<K> map) {
super(map);
}
/** Note the same entry instance is returned each time this method is called. */
@Override
public Entry<K> next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
K[] keyTable = map.keyTable;
entry.key = keyTable[nextIndex];
entry.value = map.valueTable[nextIndex];
currentIndex = nextIndex;
findNextIndex();
return entry;
}
@Override
public boolean hasNext () {
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
return hasNext;
}
@Override
public Entries<K> iterator () {
return this;
}
@Override
public void remove () {
super.remove();
}
}
static public class Values extends MapIterator<Object> {
public Values (ObjectIntMap<?> map) {
super((ObjectIntMap<Object>)map);
}
public boolean hasNext () {
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
return hasNext;
}
public int next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
int value = map.valueTable[nextIndex];
currentIndex = nextIndex;
findNextIndex();
return value;
}
/** Returns a new array containing the remaining values. */
public IntArray toArray () {
IntArray array = new IntArray(true, map.size);
while (hasNext)
array.add(next());
return array;
}
}
static public class Keys<K> extends MapIterator<K> implements Iterable<K>, Iterator<K> {
public Keys (ObjectIntMap<K> map) {
super((ObjectIntMap<K>)map);
}
@Override
public boolean hasNext () {
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
return hasNext;
}
@Override
public K next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
K key = map.keyTable[nextIndex];
currentIndex = nextIndex;
findNextIndex();
return key;
}
@Override
public Keys<K> iterator () {
return this;
}
/** Returns a new array containing the remaining keys. */
public Array<K> toArray () {
Array array = new Array(true, map.size);
while (hasNext)
array.add(next());
return array;
}
/** Adds the remaining keys to the array. */
public Array<K> toArray (Array<K> array) {
while (hasNext)
array.add(next());
return array;
}
@Override
public void remove () {
super.remove();
}
}
}

View File

@ -0,0 +1,836 @@
/*******************************************************************************
* Copyright 2011 LibGDX.
* Mario Zechner <badlogicgames@gmail.com>
* Nathan Sweet <nathan.sweet@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package dorkbox.collections;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Random;
/** An unordered map. This implementation is a cuckoo hash map using 3 hashes, random walking, and a small stash for problematic
* keys. Null keys are not allowed. Null values are allowed. No allocation is done except when growing the table size. <br>
* <br>
* This map performs very fast get, containsKey, and remove (typically O(1), worst case O(log(n))). Put may be a bit slower,
* depending on hash collisions. Load factors greater than 0.91 greatly increase the chances the map will have to rehash to the
* next higher POT size.<br>
* <br>
* Iteration can be very slow for a map with a large capacity. {@link #clear(int)} and {@link #shrink(int)} can be used to reduce
* the capacity. {@link OrderedMap} provides much faster iteration.
* @author Nathan Sweet */
@SuppressWarnings("unchecked")
public class ObjectMap<K, V> implements Iterable<ObjectMap.Entry<K, V>> {
private static final int PRIME1 = 0xbe1f14b1;
private static final int PRIME2 = 0xb4b82e39;
private static final int PRIME3 = 0xced1c241;
public int size;
K[] keyTable;
V[] valueTable;
int capacity, stashSize;
private float loadFactor;
private int hashShift, mask, threshold;
private int stashCapacity;
private int pushIterations;
private Entries entries1, entries2;
private Values values1, values2;
private Keys keys1, keys2;
/** Creates a new map with an initial capacity of 51 and a load factor of 0.8. */
public ObjectMap () {
this(51, 0.8f);
}
/** Creates a new map with a load factor of 0.8.
* @param initialCapacity If not a power of two, it is increased to the next nearest power of two. */
public ObjectMap (int initialCapacity) {
this(initialCapacity, 0.8f);
}
/** Creates a new map with the specified initial capacity and load factor. This map will hold initialCapacity items before
* growing the backing table.
* @param initialCapacity If not a power of two, it is increased to the next nearest power of two. */
public ObjectMap (int initialCapacity, float loadFactor) {
if (initialCapacity < 0) throw new IllegalArgumentException("initialCapacity must be >= 0: " + initialCapacity);
initialCapacity = MathUtil.nextPowerOfTwo((int)Math.ceil(initialCapacity / loadFactor));
if (initialCapacity > 1 << 30) throw new IllegalArgumentException("initialCapacity is too large: " + initialCapacity);
capacity = initialCapacity;
if (loadFactor <= 0) throw new IllegalArgumentException("loadFactor must be > 0: " + loadFactor);
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
mask = capacity - 1;
hashShift = 31 - Integer.numberOfTrailingZeros(capacity);
stashCapacity = Math.max(3, (int)Math.ceil(Math.log(capacity)) * 2);
pushIterations = Math.max(Math.min(capacity, 8), (int)Math.sqrt(capacity) / 8);
keyTable = (K[])new Object[capacity + stashCapacity];
valueTable = (V[])new Object[keyTable.length];
}
/** Creates a new map identical to the specified map. */
public ObjectMap (ObjectMap<? extends K, ? extends V> map) {
this((int)Math.floor(map.capacity * map.loadFactor), map.loadFactor);
stashSize = map.stashSize;
System.arraycopy(map.keyTable, 0, keyTable, 0, map.keyTable.length);
System.arraycopy(map.valueTable, 0, valueTable, 0, map.valueTable.length);
size = map.size;
}
/** Returns the old value associated with the specified key, or null. */
public V put (K key, V value) {
if (key == null) throw new IllegalArgumentException("key cannot be null.");
K[] keyTable = this.keyTable;
// Check for existing keys.
int hashCode = key.hashCode();
int index1 = hashCode & mask;
K key1 = keyTable[index1];
if (key.equals(key1)) {
V oldValue = valueTable[index1];
valueTable[index1] = value;
return oldValue;
}
int index2 = hash2(hashCode);
K key2 = keyTable[index2];
if (key.equals(key2)) {
V oldValue = valueTable[index2];
valueTable[index2] = value;
return oldValue;
}
int index3 = hash3(hashCode);
K key3 = keyTable[index3];
if (key.equals(key3)) {
V oldValue = valueTable[index3];
valueTable[index3] = value;
return oldValue;
}
// Update key in the stash.
for (int i = capacity, n = i + stashSize; i < n; i++) {
if (key.equals(keyTable[i])) {
V oldValue = valueTable[i];
valueTable[i] = value;
return oldValue;
}
}
// Check for empty buckets.
if (key1 == null) {
keyTable[index1] = key;
valueTable[index1] = value;
if (size++ >= threshold) resize(capacity << 1);
return null;
}
if (key2 == null) {
keyTable[index2] = key;
valueTable[index2] = value;
if (size++ >= threshold) resize(capacity << 1);
return null;
}
if (key3 == null) {
keyTable[index3] = key;
valueTable[index3] = value;
if (size++ >= threshold) resize(capacity << 1);
return null;
}
push(key, value, index1, key1, index2, key2, index3, key3);
return null;
}
public void putAll (ObjectMap<? extends K, ? extends V> map) {
ensureCapacity(map.size);
for (Entry<? extends K, ? extends V> entry : map)
put(entry.key, entry.value);
}
/** Skips checks for existing keys. */
private void putResize (K key, V value) {
// Check for empty buckets.
int hashCode = key.hashCode();
int index1 = hashCode & mask;
K key1 = keyTable[index1];
if (key1 == null) {
keyTable[index1] = key;
valueTable[index1] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
int index2 = hash2(hashCode);
K key2 = keyTable[index2];
if (key2 == null) {
keyTable[index2] = key;
valueTable[index2] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
int index3 = hash3(hashCode);
K key3 = keyTable[index3];
if (key3 == null) {
keyTable[index3] = key;
valueTable[index3] = value;
if (size++ >= threshold) resize(capacity << 1);
return;
}
push(key, value, index1, key1, index2, key2, index3, key3);
}
private void push (K insertKey, V insertValue, int index1, K key1, int index2, K key2, int index3, K key3) {
K[] keyTable = this.keyTable;
V[] valueTable = this.valueTable;
int mask = this.mask;
// Push keys until an empty bucket is found.
K evictedKey;
V evictedValue;
int i = 0, pushIterations = this.pushIterations;
do {
// Replace the key and value for one of the hashes.
switch (MathUtil.random.nextInt(2)) {
case 0:
evictedKey = key1;
evictedValue = valueTable[index1];
keyTable[index1] = insertKey;
valueTable[index1] = insertValue;
break;
case 1:
evictedKey = key2;
evictedValue = valueTable[index2];
keyTable[index2] = insertKey;
valueTable[index2] = insertValue;
break;
default:
evictedKey = key3;
evictedValue = valueTable[index3];
keyTable[index3] = insertKey;
valueTable[index3] = insertValue;
break;
}
// If the evicted key hashes to an empty bucket, put it there and stop.
int hashCode = evictedKey.hashCode();
index1 = hashCode & mask;
key1 = keyTable[index1];
if (key1 == null) {
keyTable[index1] = evictedKey;
valueTable[index1] = evictedValue;
if (size++ >= threshold) resize(capacity << 1);
return;
}
index2 = hash2(hashCode);
key2 = keyTable[index2];
if (key2 == null) {
keyTable[index2] = evictedKey;
valueTable[index2] = evictedValue;
if (size++ >= threshold) resize(capacity << 1);
return;
}
index3 = hash3(hashCode);
key3 = keyTable[index3];
if (key3 == null) {
keyTable[index3] = evictedKey;
valueTable[index3] = evictedValue;
if (size++ >= threshold) resize(capacity << 1);
return;
}
if (++i == pushIterations) break;
insertKey = evictedKey;
insertValue = evictedValue;
} while (true);
putStash(evictedKey, evictedValue);
}
private void putStash (K key, V value) {
if (stashSize == stashCapacity) {
// Too many pushes occurred and the stash is full, increase the table size.
resize(capacity << 1);
putResize(key, value);
return;
}
// Store key in the stash.
int index = capacity + stashSize;
keyTable[index] = key;
valueTable[index] = value;
stashSize++;
size++;
}
/** Returns the value for the specified key, or null if the key is not in the map. */
public V get (K key) {
int hashCode = key.hashCode();
int index = hashCode & mask;
if (!key.equals(keyTable[index])) {
index = hash2(hashCode);
if (!key.equals(keyTable[index])) {
index = hash3(hashCode);
if (!key.equals(keyTable[index])) return getStash(key, null);
}
}
return valueTable[index];
}
/** Returns the value for the specified key, or the default value if the key is not in the map. */
public V get (K key, V defaultValue) {
int hashCode = key.hashCode();
int index = hashCode & mask;
if (!key.equals(keyTable[index])) {
index = hash2(hashCode);
if (!key.equals(keyTable[index])) {
index = hash3(hashCode);
if (!key.equals(keyTable[index])) return getStash(key, defaultValue);
}
}
return valueTable[index];
}
private V getStash (K key, V defaultValue) {
K[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++)
if (key.equals(keyTable[i])) return valueTable[i];
return defaultValue;
}
/** Returns the value associated with the key, or null. */
public V remove (K key) {
int hashCode = key.hashCode();
int index = hashCode & mask;
if (key.equals(keyTable[index])) {
keyTable[index] = null;
V oldValue = valueTable[index];
valueTable[index] = null;
size--;
return oldValue;
}
index = hash2(hashCode);
if (key.equals(keyTable[index])) {
keyTable[index] = null;
V oldValue = valueTable[index];
valueTable[index] = null;
size--;
return oldValue;
}
index = hash3(hashCode);
if (key.equals(keyTable[index])) {
keyTable[index] = null;
V oldValue = valueTable[index];
valueTable[index] = null;
size--;
return oldValue;
}
return removeStash(key);
}
V removeStash (K key) {
K[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++) {
if (key.equals(keyTable[i])) {
V oldValue = valueTable[i];
removeStashIndex(i);
size--;
return oldValue;
}
}
return null;
}
void removeStashIndex (int index) {
// If the removed location was not last, move the last tuple to the removed location.
stashSize--;
int lastIndex = capacity + stashSize;
if (index < lastIndex) {
keyTable[index] = keyTable[lastIndex];
valueTable[index] = valueTable[lastIndex];
keyTable[lastIndex] = null;
valueTable[lastIndex] = null;
} else {
keyTable[index] = null;
valueTable[index] = null;
}
}
/** Returns true if the map is empty. */
public boolean isEmpty () {
return size == 0;
}
/** Reduces the size of the backing arrays to be the specified capacity or less. If the capacity is already less, nothing is
* done. If the map contains more items than the specified capacity, the next highest power of two capacity is used instead. */
public void shrink (int maximumCapacity) {
if (maximumCapacity < 0) throw new IllegalArgumentException("maximumCapacity must be >= 0: " + maximumCapacity);
if (size > maximumCapacity) maximumCapacity = size;
if (capacity <= maximumCapacity) return;
maximumCapacity = MathUtil.nextPowerOfTwo(maximumCapacity);
resize(maximumCapacity);
}
/** Clears the map and reduces the size of the backing arrays to be the specified capacity, if they are larger. The reduction
* is done by allocating new arrays, though for large arrays this can be faster than clearing the existing array. */
public void clear (int maximumCapacity) {
if (capacity <= maximumCapacity) {
clear();
return;
}
size = 0;
resize(maximumCapacity);
}
/** Clears the map, leaving the backing arrays at the current capacity. When the capacity is high and the population is low,
* iteration can be unnecessarily slow. {@link #clear(int)} can be used to reduce the capacity. */
public void clear () {
if (size == 0) return;
K[] keyTable = this.keyTable;
V[] valueTable = this.valueTable;
for (int i = capacity + stashSize; i-- > 0;) {
keyTable[i] = null;
valueTable[i] = null;
}
size = 0;
stashSize = 0;
}
/** Returns true if the specified value is in the map. Note this traverses the entire map and compares every value, which may
* be an expensive operation.
* @param identity If true, uses == to compare the specified value with values in the map. If false, uses
* {@link #equals(Object)}. */
public boolean containsValue (Object value, boolean identity) {
V[] valueTable = this.valueTable;
if (value == null) {
K[] keyTable = this.keyTable;
for (int i = capacity + stashSize; i-- > 0;)
if (keyTable[i] != null && valueTable[i] == null) return true;
} else if (identity) {
for (int i = capacity + stashSize; i-- > 0;)
if (valueTable[i] == value) return true;
} else {
for (int i = capacity + stashSize; i-- > 0;)
if (value.equals(valueTable[i])) return true;
}
return false;
}
public boolean containsKey (K key) {
int hashCode = key.hashCode();
int index = hashCode & mask;
if (!key.equals(keyTable[index])) {
index = hash2(hashCode);
if (!key.equals(keyTable[index])) {
index = hash3(hashCode);
if (!key.equals(keyTable[index])) return containsKeyStash(key);
}
}
return true;
}
private boolean containsKeyStash (K key) {
K[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++)
if (key.equals(keyTable[i])) return true;
return false;
}
/** Returns the key for the specified value, or null if it is not in the map. Note this traverses the entire map and compares
* every value, which may be an expensive operation.
* @param identity If true, uses == to compare the specified value with values in the map. If false, uses
* {@link #equals(Object)}. */
public K findKey (Object value, boolean identity) {
V[] valueTable = this.valueTable;
if (value == null) {
K[] keyTable = this.keyTable;
for (int i = capacity + stashSize; i-- > 0;)
if (keyTable[i] != null && valueTable[i] == null) return keyTable[i];
} else if (identity) {
for (int i = capacity + stashSize; i-- > 0;)
if (valueTable[i] == value) return keyTable[i];
} else {
for (int i = capacity + stashSize; i-- > 0;)
if (value.equals(valueTable[i])) return keyTable[i];
}
return null;
}
/** Increases the size of the backing array to accommodate the specified number of additional items. Useful before adding many
* items to avoid multiple backing array resizes. */
public void ensureCapacity (int additionalCapacity) {
if (additionalCapacity < 0) throw new IllegalArgumentException("additionalCapacity must be >= 0: " + additionalCapacity);
int sizeNeeded = size + additionalCapacity;
if (sizeNeeded >= threshold) resize(MathUtil.nextPowerOfTwo((int)Math.ceil(sizeNeeded / loadFactor)));
}
private void resize (int newSize) {
int oldEndIndex = capacity + stashSize;
capacity = newSize;
threshold = (int)(newSize * loadFactor);
mask = newSize - 1;
hashShift = 31 - Integer.numberOfTrailingZeros(newSize);
stashCapacity = Math.max(3, (int)Math.ceil(Math.log(newSize)) * 2);
pushIterations = Math.max(Math.min(newSize, 8), (int)Math.sqrt(newSize) / 8);
K[] oldKeyTable = keyTable;
V[] oldValueTable = valueTable;
keyTable = (K[])new Object[newSize + stashCapacity];
valueTable = (V[])new Object[newSize + stashCapacity];
int oldSize = size;
size = 0;
stashSize = 0;
if (oldSize > 0) {
for (int i = 0; i < oldEndIndex; i++) {
K key = oldKeyTable[i];
if (key != null) putResize(key, oldValueTable[i]);
}
}
}
private int hash2 (int h) {
h *= PRIME2;
return (h ^ h >>> hashShift) & mask;
}
private int hash3 (int h) {
h *= PRIME3;
return (h ^ h >>> hashShift) & mask;
}
@Override
public int hashCode () {
int h = 0;
K[] keyTable = this.keyTable;
V[] valueTable = this.valueTable;
for (int i = 0, n = capacity + stashSize; i < n; i++) {
K key = keyTable[i];
if (key != null) {
h += key.hashCode() * 31;
V value = valueTable[i];
if (value != null) {
h += value.hashCode();
}
}
}
return h;
}
@Override
public boolean equals (Object obj) {
if (obj == this) return true;
if (!(obj instanceof ObjectMap)) return false;
ObjectMap<K, V> other = (ObjectMap)obj;
if (other.size != size) return false;
K[] keyTable = this.keyTable;
V[] valueTable = this.valueTable;
for (int i = 0, n = capacity + stashSize; i < n; i++) {
K key = keyTable[i];
if (key != null) {
V value = valueTable[i];
if (value == null) {
if (!other.containsKey(key) || other.get(key) != null) return false;
} else {
if (!value.equals(other.get(key))) return false;
}
}
}
return true;
}
public String toString (String separator) {
return toString(separator, false);
}
@Override
public String toString () {
return toString(", ", true);
}
private String toString (String separator, boolean braces) {
if (size == 0) return braces ? "{}" : "";
StringBuilder buffer = new StringBuilder(32);
if (braces) buffer.append('{');
K[] keyTable = this.keyTable;
V[] valueTable = this.valueTable;
int i = keyTable.length;
while (i-- > 0) {
K key = keyTable[i];
if (key == null) continue;
buffer.append(key);
buffer.append('=');
buffer.append(valueTable[i]);
break;
}
while (i-- > 0) {
K key = keyTable[i];
if (key == null) continue;
buffer.append(separator);
buffer.append(key);
buffer.append('=');
buffer.append(valueTable[i]);
}
if (braces) buffer.append('}');
return buffer.toString();
}
@Override
public Entries<K, V> iterator () {
return entries();
}
/** Returns an iterator for the entries in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */
public Entries<K, V> entries () {
if (entries1 == null) {
entries1 = new Entries(this);
entries2 = new Entries(this);
}
if (!entries1.valid) {
entries1.reset();
entries1.valid = true;
entries2.valid = false;
return entries1;
}
entries2.reset();
entries2.valid = true;
entries1.valid = false;
return entries2;
}
/** Returns an iterator for the values in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Values} constructor for nested or multithreaded iteration. */
public Values<V> values () {
if (values1 == null) {
values1 = new Values(this);
values2 = new Values(this);
}
if (!values1.valid) {
values1.reset();
values1.valid = true;
values2.valid = false;
return values1;
}
values2.reset();
values2.valid = true;
values1.valid = false;
return values2;
}
/** Returns an iterator for the keys in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link Keys} constructor for nested or multithreaded iteration. */
public Keys<K> keys () {
if (keys1 == null) {
keys1 = new Keys(this);
keys2 = new Keys(this);
}
if (!keys1.valid) {
keys1.reset();
keys1.valid = true;
keys2.valid = false;
return keys1;
}
keys2.reset();
keys2.valid = true;
keys1.valid = false;
return keys2;
}
static public class Entry<K, V> {
public K key;
public V value;
@Override
public String toString () {
return key + "=" + value;
}
}
static private abstract class MapIterator<K, V, I> implements Iterable<I>, Iterator<I> {
public boolean hasNext;
final ObjectMap<K, V> map;
int nextIndex, currentIndex;
boolean valid = true;
public MapIterator (ObjectMap<K, V> map) {
this.map = map;
reset();
}
public void reset () {
currentIndex = -1;
nextIndex = -1;
findNextIndex();
}
void findNextIndex () {
hasNext = false;
K[] keyTable = map.keyTable;
for (int n = map.capacity + map.stashSize; ++nextIndex < n;) {
if (keyTable[nextIndex] != null) {
hasNext = true;
break;
}
}
}
@Override
public void remove () {
if (currentIndex < 0) throw new IllegalStateException("next must be called before remove.");
if (currentIndex >= map.capacity) {
map.removeStashIndex(currentIndex);
nextIndex = currentIndex - 1;
findNextIndex();
} else {
map.keyTable[currentIndex] = null;
map.valueTable[currentIndex] = null;
}
currentIndex = -1;
map.size--;
}
}
@SuppressWarnings("NullableProblems")
static public class Entries<K, V> extends MapIterator<K, V, Entry<K, V>> {
Entry<K, V> entry = new Entry();
public Entries (ObjectMap<K, V> map) {
super(map);
}
/** Note the same entry instance is returned each time this method is called. */
@Override
public Entry<K, V> next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
K[] keyTable = map.keyTable;
entry.key = keyTable[nextIndex];
entry.value = map.valueTable[nextIndex];
currentIndex = nextIndex;
findNextIndex();
return entry;
}
@Override
public boolean hasNext () {
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
return hasNext;
}
@Override
public Entries<K, V> iterator () {
return this;
}
}
@SuppressWarnings({"NullableProblems", "unchecked", "rawtypes"})
static public class Values<V> extends MapIterator<Object, V, V> {
public Values (ObjectMap<?, V> map) {
super((ObjectMap<Object, V>)map);
}
@Override
public boolean hasNext () {
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
return hasNext;
}
@Override
public V next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
V value = map.valueTable[nextIndex];
currentIndex = nextIndex;
findNextIndex();
return value;
}
@Override
public Values<V> iterator () {
return this;
}
/** Returns a new array containing the remaining values. */
public Array<V> toArray () {
return toArray(new Array(true, map.size));
}
/** Adds the remaining values to the specified array. */
public Array<V> toArray (Array<V> array) {
while (hasNext)
array.add(next());
return array;
}
}
@SuppressWarnings({"unchecked", "NullableProblems", "rawtypes"})
static public class Keys<K> extends MapIterator<K, Object, K> {
public Keys (ObjectMap<K, ?> map) {
super((ObjectMap<K, Object>)map);
}
@Override
public boolean hasNext () {
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
return hasNext;
}
@Override
public K next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
K key = map.keyTable[nextIndex];
currentIndex = nextIndex;
findNextIndex();
return key;
}
@Override
public Keys<K> iterator () {
return this;
}
/** Returns a new array containing the remaining keys. */
public Array<K> toArray () {
return toArray(new Array(true, map.size));
}
/** Adds the remaining keys to the array. */
public Array<K> toArray (Array<K> array) {
while (hasNext)
array.add(next());
return array;
}
}
}

View File

@ -0,0 +1,589 @@
/*******************************************************************************
* Copyright 2011 LibGDX.
* Mario Zechner <badlogicgames@gmail.com>
* Nathan Sweet <nathan.sweet@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package dorkbox.collections;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Random;
/** An unordered set where the keys are objects. This implementation uses cuckoo hashing using 3 hashes, random walking, and a
* small stash for problematic keys. Null keys are not allowed. No allocation is done except when growing the table size. <br>
* <br>
* This set performs very fast contains and remove (typically O(1), worst case O(log(n))). Add may be a bit slower, depending on
* hash collisions. Load factors greater than 0.91 greatly increase the chances the set will have to rehash to the next higher POT
* size.<br>
* <br>
* Iteration can be very slow for a set with a large capacity. {@link #clear(int)} and {@link #shrink(int)} can be used to reduce
* the capacity. {@link OrderedSet} provides much faster iteration.
* @author Nathan Sweet */
@SuppressWarnings({"unchecked", "rawtypes", "NullableProblems", "SuspiciousSystemArraycopy"})
public class ObjectSet<T> implements Iterable<T> {
private static final int PRIME1 = 0xbe1f14b1;
private static final int PRIME2 = 0xb4b82e39;
private static final int PRIME3 = 0xced1c241;
public int size;
T[] keyTable;
int capacity, stashSize;
private float loadFactor;
private int hashShift, mask, threshold;
private int stashCapacity;
private int pushIterations;
private ObjectSetIterator iterator1, iterator2;
/** Creates a new set with an initial capacity of 51 and a load factor of 0.8. */
public ObjectSet () {
this(51, 0.8f);
}
/** Creates a new set with a load factor of 0.8.
* @param initialCapacity If not a power of two, it is increased to the next nearest power of two. */
public ObjectSet (int initialCapacity) {
this(initialCapacity, 0.8f);
}
/** Creates a new set with the specified initial capacity and load factor. This set will hold initialCapacity items before
* growing the backing table.
* @param initialCapacity If not a power of two, it is increased to the next nearest power of two. */
public ObjectSet (int initialCapacity, float loadFactor) {
if (initialCapacity < 0) throw new IllegalArgumentException("initialCapacity must be >= 0: " + initialCapacity);
initialCapacity = MathUtil.nextPowerOfTwo((int)Math.ceil(initialCapacity / loadFactor));
if (initialCapacity > 1 << 30) throw new IllegalArgumentException("initialCapacity is too large: " + initialCapacity);
capacity = initialCapacity;
if (loadFactor <= 0) throw new IllegalArgumentException("loadFactor must be > 0: " + loadFactor);
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
mask = capacity - 1;
hashShift = 31 - Integer.numberOfTrailingZeros(capacity);
stashCapacity = Math.max(3, (int)Math.ceil(Math.log(capacity)) * 2);
pushIterations = Math.max(Math.min(capacity, 8), (int)Math.sqrt(capacity) / 8);
keyTable = (T[])new Object[capacity + stashCapacity];
}
/** Creates a new set identical to the specified set. */
public ObjectSet (ObjectSet set) {
this((int)Math.floor(set.capacity * set.loadFactor), set.loadFactor);
stashSize = set.stashSize;
System.arraycopy(set.keyTable, 0, keyTable, 0, set.keyTable.length);
size = set.size;
}
/** Returns true if the key was not already in the set. If this set already contains the key, the call leaves the set unchanged
* and returns false. */
public boolean add (T key) {
if (key == null) throw new IllegalArgumentException("key cannot be null.");
T[] keyTable = this.keyTable;
// Check for existing keys.
int hashCode = key.hashCode();
int index1 = hashCode & mask;
T key1 = keyTable[index1];
if (key.equals(key1)) return false;
int index2 = hash2(hashCode);
T key2 = keyTable[index2];
if (key.equals(key2)) return false;
int index3 = hash3(hashCode);
T key3 = keyTable[index3];
if (key.equals(key3)) return false;
// Find key in the stash.
for (int i = capacity, n = i + stashSize; i < n; i++)
if (key.equals(keyTable[i])) return false;
// Check for empty buckets.
if (key1 == null) {
keyTable[index1] = key;
if (size++ >= threshold) resize(capacity << 1);
return true;
}
if (key2 == null) {
keyTable[index2] = key;
if (size++ >= threshold) resize(capacity << 1);
return true;
}
if (key3 == null) {
keyTable[index3] = key;
if (size++ >= threshold) resize(capacity << 1);
return true;
}
push(key, index1, key1, index2, key2, index3, key3);
return true;
}
public void addAll (Array<? extends T> array) {
addAll(array.items, 0, array.size);
}
public void addAll (Array<? extends T> array, int offset, int length) {
if (offset + length > array.size)
throw new IllegalArgumentException("offset + length must be <= size: " + offset + " + " + length + " <= " + array.size);
addAll((T[])array.items, offset, length);
}
public void addAll (T... array) {
addAll(array, 0, array.length);
}
public void addAll (T[] array, int offset, int length) {
ensureCapacity(length);
for (int i = offset, n = i + length; i < n; i++)
add(array[i]);
}
public void addAll (ObjectSet<T> set) {
ensureCapacity(set.size);
for (T key : set)
add(key);
}
/** Skips checks for existing keys. */
private void addResize (T key) {
// Check for empty buckets.
int hashCode = key.hashCode();
int index1 = hashCode & mask;
T key1 = keyTable[index1];
if (key1 == null) {
keyTable[index1] = key;
if (size++ >= threshold) resize(capacity << 1);
return;
}
int index2 = hash2(hashCode);
T key2 = keyTable[index2];
if (key2 == null) {
keyTable[index2] = key;
if (size++ >= threshold) resize(capacity << 1);
return;
}
int index3 = hash3(hashCode);
T key3 = keyTable[index3];
if (key3 == null) {
keyTable[index3] = key;
if (size++ >= threshold) resize(capacity << 1);
return;
}
push(key, index1, key1, index2, key2, index3, key3);
}
private void push (T insertKey, int index1, T key1, int index2, T key2, int index3, T key3) {
T[] keyTable = this.keyTable;
int mask = this.mask;
// Push keys until an empty bucket is found.
T evictedKey;
int i = 0, pushIterations = this.pushIterations;
do {
// Replace the key and value for one of the hashes.
switch (MathUtil.random.nextInt(2)) {
case 0:
evictedKey = key1;
keyTable[index1] = insertKey;
break;
case 1:
evictedKey = key2;
keyTable[index2] = insertKey;
break;
default:
evictedKey = key3;
keyTable[index3] = insertKey;
break;
}
// If the evicted key hashes to an empty bucket, put it there and stop.
int hashCode = evictedKey.hashCode();
index1 = hashCode & mask;
key1 = keyTable[index1];
if (key1 == null) {
keyTable[index1] = evictedKey;
if (size++ >= threshold) resize(capacity << 1);
return;
}
index2 = hash2(hashCode);
key2 = keyTable[index2];
if (key2 == null) {
keyTable[index2] = evictedKey;
if (size++ >= threshold) resize(capacity << 1);
return;
}
index3 = hash3(hashCode);
key3 = keyTable[index3];
if (key3 == null) {
keyTable[index3] = evictedKey;
if (size++ >= threshold) resize(capacity << 1);
return;
}
if (++i == pushIterations) break;
insertKey = evictedKey;
} while (true);
addStash(evictedKey);
}
private void addStash (T key) {
if (stashSize == stashCapacity) {
// Too many pushes occurred and the stash is full, increase the table size.
resize(capacity << 1);
addResize(key);
return;
}
// Store key in the stash.
int index = capacity + stashSize;
keyTable[index] = key;
stashSize++;
size++;
}
/** Returns true if the key was removed. */
public boolean remove (T key) {
int hashCode = key.hashCode();
int index = hashCode & mask;
if (key.equals(keyTable[index])) {
keyTable[index] = null;
size--;
return true;
}
index = hash2(hashCode);
if (key.equals(keyTable[index])) {
keyTable[index] = null;
size--;
return true;
}
index = hash3(hashCode);
if (key.equals(keyTable[index])) {
keyTable[index] = null;
size--;
return true;
}
return removeStash(key);
}
boolean removeStash (T key) {
T[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++) {
if (key.equals(keyTable[i])) {
removeStashIndex(i);
size--;
return true;
}
}
return false;
}
void removeStashIndex (int index) {
// If the removed location was not last, move the last tuple to the removed location.
stashSize--;
int lastIndex = capacity + stashSize;
if (index < lastIndex) {
keyTable[index] = keyTable[lastIndex];
keyTable[lastIndex] = null;
}
}
/** Returns true if the set is empty. */
public boolean isEmpty () {
return size == 0;
}
/** Reduces the size of the backing arrays to be the specified capacity or less. If the capacity is already less, nothing is
* done. If the set contains more items than the specified capacity, the next highest power of two capacity is used instead. */
public void shrink (int maximumCapacity) {
if (maximumCapacity < 0) throw new IllegalArgumentException("maximumCapacity must be >= 0: " + maximumCapacity);
if (size > maximumCapacity) maximumCapacity = size;
if (capacity <= maximumCapacity) return;
maximumCapacity = MathUtil.nextPowerOfTwo(maximumCapacity);
resize(maximumCapacity);
}
/** Clears the set and reduces the size of the backing arrays to be the specified capacity, if they are larger. The reduction
* is done by allocating new arrays, though for large arrays this can be faster than clearing the existing array. */
public void clear (int maximumCapacity) {
if (capacity <= maximumCapacity) {
clear();
return;
}
size = 0;
resize(maximumCapacity);
}
/** Clears the set, leaving the backing arrays at the current capacity. When the capacity is high and the population is low,
* iteration can be unnecessarily slow. {@link #clear(int)} can be used to reduce the capacity. */
public void clear () {
if (size == 0) return;
T[] keyTable = this.keyTable;
for (int i = capacity + stashSize; i-- > 0;)
keyTable[i] = null;
size = 0;
stashSize = 0;
}
public boolean contains (T key) {
int hashCode = key.hashCode();
int index = hashCode & mask;
if (!key.equals(keyTable[index])) {
index = hash2(hashCode);
if (!key.equals(keyTable[index])) {
index = hash3(hashCode);
if (!key.equals(keyTable[index])) return getKeyStash(key) != null;
}
}
return true;
}
/** @return May be null. */
public T get (T key) {
int hashCode = key.hashCode();
int index = hashCode & mask;
T found = keyTable[index];
if (!key.equals(found)) {
index = hash2(hashCode);
found = keyTable[index];
if (!key.equals(found)) {
index = hash3(hashCode);
found = keyTable[index];
if (!key.equals(found)) return getKeyStash(key);
}
}
return found;
}
private T getKeyStash (T key) {
T[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++)
if (key.equals(keyTable[i])) return keyTable[i];
return null;
}
public T first () {
T[] keyTable = this.keyTable;
for (int i = 0, n = capacity + stashSize; i < n; i++)
if (keyTable[i] != null) return keyTable[i];
throw new IllegalStateException("ObjectSet is empty.");
}
/** Increases the size of the backing array to accommodate the specified number of additional items. Useful before adding many
* items to avoid multiple backing array resizes. */
public void ensureCapacity (int additionalCapacity) {
if (additionalCapacity < 0) throw new IllegalArgumentException("additionalCapacity must be >= 0: " + additionalCapacity);
int sizeNeeded = size + additionalCapacity;
if (sizeNeeded >= threshold) resize(MathUtil.nextPowerOfTwo((int)Math.ceil(sizeNeeded / loadFactor)));
}
private void resize (int newSize) {
int oldEndIndex = capacity + stashSize;
capacity = newSize;
threshold = (int)(newSize * loadFactor);
mask = newSize - 1;
hashShift = 31 - Integer.numberOfTrailingZeros(newSize);
stashCapacity = Math.max(3, (int)Math.ceil(Math.log(newSize)) * 2);
pushIterations = Math.max(Math.min(newSize, 8), (int)Math.sqrt(newSize) / 8);
T[] oldKeyTable = keyTable;
keyTable = (T[])new Object[newSize + stashCapacity];
int oldSize = size;
size = 0;
stashSize = 0;
if (oldSize > 0) {
for (int i = 0; i < oldEndIndex; i++) {
T key = oldKeyTable[i];
if (key != null) addResize(key);
}
}
}
private int hash2 (int h) {
h *= PRIME2;
return (h ^ h >>> hashShift) & mask;
}
private int hash3 (int h) {
h *= PRIME3;
return (h ^ h >>> hashShift) & mask;
}
@Override
public int hashCode () {
int h = 0;
for (int i = 0, n = capacity + stashSize; i < n; i++)
if (keyTable[i] != null) h += keyTable[i].hashCode();
return h;
}
@Override
public boolean equals (Object obj) {
if (!(obj instanceof ObjectSet)) return false;
ObjectSet other = (ObjectSet)obj;
if (other.size != size) return false;
T[] keyTable = this.keyTable;
for (int i = 0, n = capacity + stashSize; i < n; i++)
if (keyTable[i] != null && !other.contains(keyTable[i])) return false;
return true;
}
@Override
public String toString () {
return '{' + toString(", ") + '}';
}
public String toString (String separator) {
if (size == 0) return "";
StringBuilder buffer = new StringBuilder(32);
T[] keyTable = this.keyTable;
int i = keyTable.length;
while (i-- > 0) {
T key = keyTable[i];
if (key == null) continue;
buffer.append(key);
break;
}
while (i-- > 0) {
T key = keyTable[i];
if (key == null) continue;
buffer.append(separator);
buffer.append(key);
}
return buffer.toString();
}
/** Returns an iterator for the keys in the set. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link ObjectSetIterator} constructor for nested or multithreaded iteration. */
@Override
public ObjectSetIterator<T> iterator () {
if (iterator1 == null) {
iterator1 = new ObjectSetIterator(this);
iterator2 = new ObjectSetIterator(this);
}
if (!iterator1.valid) {
iterator1.reset();
iterator1.valid = true;
iterator2.valid = false;
return iterator1;
}
iterator2.reset();
iterator2.valid = true;
iterator1.valid = false;
return iterator2;
}
static public <T> ObjectSet<T> with (T... array) {
ObjectSet set = new ObjectSet();
set.addAll(array);
return set;
}
static public class ObjectSetIterator<K> implements Iterable<K>, Iterator<K> {
public boolean hasNext;
final ObjectSet<K> set;
int nextIndex, currentIndex;
boolean valid = true;
public ObjectSetIterator (ObjectSet<K> set) {
this.set = set;
reset();
}
public void reset () {
currentIndex = -1;
nextIndex = -1;
findNextIndex();
}
private void findNextIndex () {
hasNext = false;
K[] keyTable = set.keyTable;
for (int n = set.capacity + set.stashSize; ++nextIndex < n;) {
if (keyTable[nextIndex] != null) {
hasNext = true;
break;
}
}
}
@Override
public void remove () {
if (currentIndex < 0) throw new IllegalStateException("next must be called before remove.");
if (currentIndex >= set.capacity) {
set.removeStashIndex(currentIndex);
nextIndex = currentIndex - 1;
findNextIndex();
} else {
set.keyTable[currentIndex] = null;
}
currentIndex = -1;
set.size--;
}
@Override
public boolean hasNext () {
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
return hasNext;
}
@Override
public K next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
K key = set.keyTable[nextIndex];
currentIndex = nextIndex;
findNextIndex();
return key;
}
@Override
public ObjectSetIterator<K> iterator () {
return this;
}
/** Adds the remaining values to the array. */
public Array<K> toArray (Array<K> array) {
while (hasNext)
array.add(next());
return array;
}
/** Returns a new array containing the remaining values. */
public Array<K> toArray () {
return toArray(new Array(true, set.size));
}
}
}

View File

@ -0,0 +1,271 @@
/*******************************************************************************
* Copyright 2011 LibGDX.
* Mario Zechner <badlogicgames@gmail.com>
* Nathan Sweet <nathan.sweet@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package dorkbox.collections;
import java.util.NoSuchElementException;
/** An {@link ObjectMap} that also stores keys in an {@link Array} using the insertion order. Iteration over the
* {@link #entries()}, {@link #keys()}, and {@link #values()} is ordered and faster than an unordered map. Keys can also be
* accessed and the order changed using {@link #orderedKeys()}. There is some additional overhead for put and remove. When used
* for faster iteration versus ObjectMap and the order does not actually matter, copying during remove can be greatly reduced by
* setting {@link Array#ordered} to false for {@link OrderedMap#orderedKeys()}.
* @author Nathan Sweet */
@SuppressWarnings({"unchecked", "NullableProblems"})
public class OrderedMap<K, V> extends ObjectMap<K, V> {
final Array<K> keys;
private Entries entries1, entries2;
private Values values1, values2;
private Keys keys1, keys2;
public OrderedMap () {
keys = new Array();
}
public OrderedMap (int initialCapacity) {
super(initialCapacity);
keys = new Array(capacity);
}
public OrderedMap (int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
keys = new Array(capacity);
}
public OrderedMap (OrderedMap<? extends K, ? extends V> map) {
super(map);
keys = new Array(map.keys);
}
@Override
public V put (K key, V value) {
if (!containsKey(key)) keys.add(key);
return super.put(key, value);
}
@Override
public V remove (K key) {
keys.removeValue(key, false);
return super.remove(key);
}
public V removeIndex (int index) {
return super.remove(keys.removeIndex(index));
}
@Override
public void clear (int maximumCapacity) {
keys.clear();
super.clear(maximumCapacity);
}
@Override
public void clear () {
keys.clear();
super.clear();
}
public Array<K> orderedKeys () {
return keys;
}
@Override
public Entries<K, V> iterator () {
return entries();
}
/** Returns an iterator for the entries in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link OrderedMapEntries} constructor for nested or multithreaded iteration. */
@Override
public Entries<K, V> entries () {
if (entries1 == null) {
entries1 = new OrderedMapEntries(this);
entries2 = new OrderedMapEntries(this);
}
if (!entries1.valid) {
entries1.reset();
entries1.valid = true;
entries2.valid = false;
return entries1;
}
entries2.reset();
entries2.valid = true;
entries1.valid = false;
return entries2;
}
/** Returns an iterator for the values in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link OrderedMapValues} constructor for nested or multithreaded iteration. */
@Override
public Values<V> values () {
if (values1 == null) {
values1 = new OrderedMapValues(this);
values2 = new OrderedMapValues(this);
}
if (!values1.valid) {
values1.reset();
values1.valid = true;
values2.valid = false;
return values1;
}
values2.reset();
values2.valid = true;
values1.valid = false;
return values2;
}
/** Returns an iterator for the keys in the map. Remove is supported. Note that the same iterator instance is returned each
* time this method is called. Use the {@link OrderedMapKeys} constructor for nested or multithreaded iteration. */
@Override
public Keys<K> keys () {
if (keys1 == null) {
keys1 = new OrderedMapKeys(this);
keys2 = new OrderedMapKeys(this);
}
if (!keys1.valid) {
keys1.reset();
keys1.valid = true;
keys2.valid = false;
return keys1;
}
keys2.reset();
keys2.valid = true;
keys1.valid = false;
return keys2;
}
@Override
public String toString () {
if (size == 0) return "{}";
StringBuilder buffer = new StringBuilder(32);
buffer.append('{');
Array<K> keys = this.keys;
for (int i = 0, n = keys.size; i < n; i++) {
K key = keys.get(i);
if (i > 0) buffer.append(", ");
buffer.append(key);
buffer.append('=');
buffer.append(get(key));
}
buffer.append('}');
return buffer.toString();
}
@SuppressWarnings("rawtypes")
static public class OrderedMapEntries<K, V> extends Entries<K, V> {
private final Array<K> keys;
public OrderedMapEntries (OrderedMap<K, V> map) {
super(map);
keys = map.keys;
}
@Override
public void reset () {
nextIndex = 0;
hasNext = map.size > 0;
}
@Override
public Entry next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
entry.key = keys.get(nextIndex);
entry.value = map.get(entry.key);
nextIndex++;
hasNext = nextIndex < map.size;
return entry;
}
@Override
public void remove () {
if (currentIndex < 0) throw new IllegalStateException("next must be called before remove.");
map.remove(entry.key);
nextIndex--;
}
}
static public class OrderedMapKeys<K> extends Keys<K> {
private final Array<K> keys;
public OrderedMapKeys (OrderedMap<K, ?> map) {
super(map);
keys = map.keys;
}
@Override
public void reset () {
nextIndex = 0;
hasNext = map.size > 0;
}
@Override
public K next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
K key = keys.get(nextIndex);
currentIndex = nextIndex;
nextIndex++;
hasNext = nextIndex < map.size;
return key;
}
@Override
public void remove () {
if (currentIndex < 0) throw new IllegalStateException("next must be called before remove.");
((OrderedMap)map).removeIndex(nextIndex - 1);
nextIndex = currentIndex;
currentIndex = -1;
}
}
static public class OrderedMapValues<V> extends Values<V> {
private final Array keys;
public OrderedMapValues (OrderedMap<?, V> map) {
super(map);
keys = map.keys;
}
@Override
public void reset () {
nextIndex = 0;
hasNext = map.size > 0;
}
@Override
public V next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
V value = (V)map.get(keys.get(nextIndex));
currentIndex = nextIndex;
nextIndex++;
hasNext = nextIndex < map.size;
return value;
}
@Override
public void remove () {
if (currentIndex < 0) throw new IllegalStateException("next must be called before remove.");
((OrderedMap)map).removeIndex(currentIndex);
nextIndex = currentIndex;
currentIndex = -1;
}
}
}

View File

@ -0,0 +1,169 @@
/*******************************************************************************
* Copyright 2011 LibGDX.
* Mario Zechner <badlogicgames@gmail.com>
* Nathan Sweet <nathan.sweet@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package dorkbox.collections;
import java.util.NoSuchElementException;
/** An {@link ObjectSet} that also stores keys in an {@link Array} using the insertion order. {@link #iterator() Iteration} is
* ordered and faster than an unordered set. Keys can also be accessed and the order changed using {@link #orderedItems()}. There
* is some additional overhead for put and remove. When used for faster iteration versus ObjectSet and the order does not actually
* matter, copying during remove can be greatly reduced by setting {@link Array#ordered} to false for
* {@link OrderedSet#orderedItems()}.
* @author Nathan Sweet */
@SuppressWarnings({"unchecked", "rawtypes"})
public class OrderedSet<T> extends ObjectSet<T> {
final Array<T> items;
OrderedSetIterator iterator1, iterator2;
public OrderedSet () {
items = new Array();
}
public OrderedSet (int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
items = new Array(capacity);
}
public OrderedSet (int initialCapacity) {
super(initialCapacity);
items = new Array(capacity);
}
public OrderedSet (OrderedSet set) {
super(set);
items = new Array(capacity);
items.addAll(set.items);
}
@Override
public boolean add (T key) {
if (!super.add(key)) return false;
items.add(key);
return true;
}
public boolean add (T key, int index) {
if (!super.add(key)) {
items.removeValue(key, true);
items.insert(index, key);
return false;
}
items.insert(index, key);
return true;
}
@Override
public boolean remove (T key) {
if (!super.remove(key)) return false;
items.removeValue(key, false);
return true;
}
public T removeIndex (int index) {
T key = items.removeIndex(index);
super.remove(key);
return key;
}
@Override
public void clear (int maximumCapacity) {
items.clear();
super.clear(maximumCapacity);
}
@Override
public void clear () {
items.clear();
super.clear();
}
public Array<T> orderedItems () {
return items;
}
@Override
public OrderedSetIterator<T> iterator () {
if (iterator1 == null) {
iterator1 = new OrderedSetIterator(this);
iterator2 = new OrderedSetIterator(this);
}
if (!iterator1.valid) {
iterator1.reset();
iterator1.valid = true;
iterator2.valid = false;
return iterator1;
}
iterator2.reset();
iterator2.valid = true;
iterator1.valid = false;
return iterator2;
}
@Override
public String toString () {
if (size == 0) return "{}";
T[] items = this.items.items;
StringBuilder buffer = new StringBuilder(32);
buffer.append('{');
buffer.append(items[0]);
for (int i = 1; i < size; i++) {
buffer.append(", ");
buffer.append(items[i]);
}
buffer.append('}');
return buffer.toString();
}
@Override
public String toString (String separator) {
return items.toString(separator);
}
static public class OrderedSetIterator<T> extends ObjectSetIterator<T> {
private Array<T> items;
public OrderedSetIterator (OrderedSet<T> set) {
super(set);
items = set.items;
}
@Override
public void reset () {
nextIndex = 0;
hasNext = set.size > 0;
}
@Override
public T next () {
if (!hasNext) throw new NoSuchElementException();
if (!valid) throw new RuntimeException("#iterator() cannot be used nested.");
T key = items.get(nextIndex);
nextIndex++;
hasNext = nextIndex < set.size;
return key;
}
@Override
public void remove () {
if (nextIndex < 0) throw new IllegalStateException("next must be called before remove.");
nextIndex--;
((OrderedSet)set).removeIndex(nextIndex);
}
}
}

View File

@ -0,0 +1,113 @@
/*******************************************************************************
* Copyright 2011 LibGDX.
* Mario Zechner <badlogicgames@gmail.com>
* Nathan Sweet <nathan.sweet@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package dorkbox.collections;
import java.util.Iterator;
/** Interface used to select items within an iterator against a predicate.
* @author Xoppa */
public interface Predicate<T> {
/** @return true if the item matches the criteria and should be included in the iterator's items */
boolean evaluate (T arg0);
public class PredicateIterator<T> implements Iterator<T> {
public Iterator<T> iterator;
public Predicate<T> predicate;
public boolean end = false;
public boolean peeked = false;
public T next = null;
public PredicateIterator (final Iterable<T> iterable, final Predicate<T> predicate) {
this(iterable.iterator(), predicate);
}
public PredicateIterator (final Iterator<T> iterator, final Predicate<T> predicate) {
set(iterator, predicate);
}
public void set (final Iterable<T> iterable, final Predicate<T> predicate) {
set(iterable.iterator(), predicate);
}
public void set (final Iterator<T> iterator, final Predicate<T> predicate) {
this.iterator = iterator;
this.predicate = predicate;
end = peeked = false;
next = null;
}
@Override
public boolean hasNext () {
if (end) return false;
if (next != null) return true;
peeked = true;
while (iterator.hasNext()) {
final T n = iterator.next();
if (predicate.evaluate(n)) {
next = n;
return true;
}
}
end = true;
return false;
}
@Override
public T next () {
if (next == null && !hasNext()) return null;
final T result = next;
next = null;
peeked = false;
return result;
}
@Override
public void remove () {
if (peeked) throw new RuntimeException("Cannot remove between a call to hasNext() and next().");
iterator.remove();
}
}
public static class PredicateIterable<T> implements Iterable<T> {
public Iterable<T> iterable;
public Predicate<T> predicate;
public PredicateIterator<T> iterator = null;
public PredicateIterable (Iterable<T> iterable, Predicate<T> predicate) {
set(iterable, predicate);
}
public void set (Iterable<T> iterable, Predicate<T> predicate) {
this.iterable = iterable;
this.predicate = predicate;
}
/** Returns an iterator. Note that the same iterator instance is returned each time this method is called. Use the
* {@link Predicate.PredicateIterator} constructor for nested or multithreaded iteration. */
@Override
public Iterator<T> iterator () {
if (iterator == null)
iterator = new PredicateIterator<T>(iterable.iterator(), predicate);
else
iterator.set(iterable.iterator(), predicate);
return iterator;
}
}
}

View File

@ -0,0 +1,100 @@
/*******************************************************************************
* Copyright 2011 LibGDX.
* Mario Zechner <badlogicgames@gmail.com>
* Nathan Sweet <nathan.sweet@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package dorkbox.collections;
import java.util.Comparator;
/** Implementation of Tony Hoare's quickselect algorithm. Running time is generally O(n), but worst case is O(n^2) Pivot choice is
* median of three method, providing better performance than a random pivot for partially sorted data.
* http://en.wikipedia.org/wiki/Quickselect
* @author Jon Renner */
public class QuickSelect<T> {
private T[] array;
private Comparator<? super T> comp;
public int select (T[] items, Comparator<T> comp, int n, int size) {
this.array = items;
this.comp = comp;
return recursiveSelect(0, size - 1, n);
}
private int partition (int left, int right, int pivot) {
T pivotValue = array[pivot];
swap(right, pivot);
int storage = left;
for (int i = left; i < right; i++) {
if (comp.compare(array[i], pivotValue) < 0) {
swap(storage, i);
storage++;
}
}
swap(right, storage);
return storage;
}
private int recursiveSelect (int left, int right, int k) {
if (left == right) return left;
int pivotIndex = medianOfThreePivot(left, right);
int pivotNewIndex = partition(left, right, pivotIndex);
int pivotDist = (pivotNewIndex - left) + 1;
int result;
if (pivotDist == k) {
result = pivotNewIndex;
} else if (k < pivotDist) {
result = recursiveSelect(left, pivotNewIndex - 1, k);
} else {
result = recursiveSelect(pivotNewIndex + 1, right, k - pivotDist);
}
return result;
}
/** Median of Three has the potential to outperform a random pivot, especially for partially sorted arrays */
private int medianOfThreePivot (int leftIdx, int rightIdx) {
T left = array[leftIdx];
int midIdx = (leftIdx + rightIdx) / 2;
T mid = array[midIdx];
T right = array[rightIdx];
// spaghetti median of three algorithm
// does at most 3 comparisons
if (comp.compare(left, mid) > 0) {
if (comp.compare(mid, right) > 0) {
return midIdx;
} else if (comp.compare(left, right) > 0) {
return rightIdx;
} else {
return leftIdx;
}
} else {
if (comp.compare(left, right) > 0) {
return leftIdx;
} else if (comp.compare(mid, right) > 0) {
return rightIdx;
} else {
return midIdx;
}
}
}
private void swap (int left, int right) {
T tmp = array[left];
array[left] = array[right];
array[right] = tmp;
}
}

View File

@ -0,0 +1,96 @@
/*******************************************************************************
* Copyright 2011 LibGDX.
* Mario Zechner <badlogicgames@gmail.com>
* Nathan Sweet <nathan.sweet@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package dorkbox.collections;
import java.util.Comparator;
/** This class is for selecting a ranked element (kth ordered statistic) from an unordered list in faster time than sorting the
* whole array. Typical applications include finding the nearest enemy unit(s), and other operations which are likely to run as
* often as every x frames. Certain values of k will result in a partial sorting of the Array.
* <p>
* The lowest ranking element starts at 1, not 0. 1 = first, 2 = second, 3 = third, etc. calling with a value of zero will result
* in a {@link RuntimeException}
* </p>
* <p>
* This class uses very minimal extra memory, as it makes no copies of the array. The underlying algorithms used are a naive
* single-pass for k=min and k=max, and Hoare's quickselect for values in between.
* </p>
* @author Jon Renner */
@SuppressWarnings("unchecked")
public class Select {
private static Select instance;
private QuickSelect quickSelect;
/** Provided for convenience */
public static Select instance () {
if (instance == null) instance = new Select();
return instance;
}
public <T> T select (T[] items, Comparator<T> comp, int kthLowest, int size) {
int idx = selectIndex(items, comp, kthLowest, size);
return items[idx];
}
public <T> int selectIndex (T[] items, Comparator<T> comp, int kthLowest, int size) {
if (size < 1) {
throw new RuntimeException("cannot select from empty array (size < 1)");
} else if (kthLowest > size) {
throw new RuntimeException("Kth rank is larger than size. k: " + kthLowest + ", size: " + size);
}
int idx;
// naive partial selection sort almost certain to outperform quickselect where n is min or max
if (kthLowest == 1) {
// find min
idx = fastMin(items, comp, size);
} else if (kthLowest == size) {
// find max
idx = fastMax(items, comp, size);
} else {
// quickselect a better choice for cases of k between min and max
if (quickSelect == null) quickSelect = new QuickSelect();
idx = quickSelect.select(items, comp, kthLowest, size);
}
return idx;
}
/** Faster than quickselect for n = min */
private <T> int fastMin (T[] items, Comparator<T> comp, int size) {
int lowestIdx = 0;
for (int i = 1; i < size; i++) {
int comparison = comp.compare(items[i], items[lowestIdx]);
if (comparison < 0) {
lowestIdx = i;
}
}
return lowestIdx;
}
/** Faster than quickselect for n = max */
private <T> int fastMax (T[] items, Comparator<T> comp, int size) {
int highestIdx = 0;
for (int i = 1; i < size; i++) {
int comparison = comp.compare(items[i], items[highestIdx]);
if (comparison > 0) {
highestIdx = i;
}
}
return highestIdx;
}
}

View File

@ -0,0 +1,66 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS"
* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
package dorkbox.collections;
import java.util.Comparator;
/** Provides methods to sort arrays of objects. Sorting requires working memory and this class allows that memory to be reused to
* avoid allocation. The sorting is otherwise identical to the Arrays.sort methods (uses timsort).<br>
* <br>
* Note that sorting primitive arrays with the Arrays.sort methods does not allocate memory (unless sorting large arrays of char,
* short, or byte).
* @author Nathan Sweet */
@SuppressWarnings({"RedundantCast", "unchecked", "rawtypes"})
public class Sort {
static private Sort instance;
private TimSort timSort;
private ComparableTimSort comparableTimSort;
public <T> void sort (Array<T> a) {
if (comparableTimSort == null) comparableTimSort = new ComparableTimSort();
comparableTimSort.doSort((Object[])a.items, 0, a.size);
}
public <T> void sort (T[] a) {
if (comparableTimSort == null) comparableTimSort = new ComparableTimSort();
comparableTimSort.doSort(a, 0, a.length);
}
public <T> void sort (T[] a, int fromIndex, int toIndex) {
if (comparableTimSort == null) comparableTimSort = new ComparableTimSort();
comparableTimSort.doSort(a, fromIndex, toIndex);
}
public <T> void sort (Array<T> a, Comparator<? super T> c) {
if (timSort == null) timSort = new TimSort();
timSort.doSort((Object[])a.items, (Comparator)c, 0, a.size);
}
public <T> void sort (T[] a, Comparator<? super T> c) {
if (timSort == null) timSort = new TimSort();
timSort.doSort(a, c, 0, a.length);
}
public <T> void sort (T[] a, Comparator<? super T> c, int fromIndex, int toIndex) {
if (timSort == null) timSort = new TimSort();
timSort.doSort(a, c, fromIndex, toIndex);
}
/** Returns a Sort instance for convenience. Multiple threads must not use this instance at the same time. */
static public Sort instance () {
if (instance == null) instance = new Sort();
return instance;
}
}

View File

@ -0,0 +1,840 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS"
* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
package dorkbox.collections;
import java.util.Arrays;
import java.util.Comparator;
/** A stable, adaptive, iterative mergesort that requires far fewer than n lg(n) comparisons when running on partially sorted
* arrays, while offering performance comparable to a traditional mergesort when run on random arrays. Like all proper mergesorts,
* this sort is stable and runs O(n log n) time (worst case). In the worst case, this sort requires temporary storage space for
* n/2 object references; in the best case, it requires only a small constant amount of space.
*
* This implementation was adapted from Tim Peters's list sort for Python, which is described in detail here:
*
* http://svn.python.org/projects/python/trunk/Objects/listsort.txt
*
* Tim's C code may be found here:
*
* http://svn.python.org/projects/python/trunk/Objects/listobject.c
*
* The underlying techniques are described in this paper (and may have even earlier origins):
*
* "Optimistic Sorting and Information Theoretic Complexity" Peter McIlroy SODA (Fourth Annual ACM-SIAM Symposium on Discrete
* Algorithms), pp 467-474, Austin, Texas, 25-27 January 1993.
*
* While the API to this class consists solely of static methods, it is (privately) instantiable; a TimSort instance holds the
* state of an ongoing sort, assuming the input array is large enough to warrant the full-blown TimSort. Small arrays are sorted
* in place, using a binary insertion sort. */
@SuppressWarnings("unchecked")
class TimSort<T> {
/** This is the minimum sized sequence that will be merged. Shorter sequences will be lengthened by calling binarySort. If the
* entire array is less than this length, no merges will be performed.
*
* This constant should be a power of two. It was 64 in Tim Peter's C implementation, but 32 was empirically determined to work
* better in this implementation. In the unlikely event that you set this constant to be a number that's not a power of two,
* you'll need to change the {@link #minRunLength} computation.
*
* If you decrease this constant, you must change the stackLen computation in the TimSort constructor, or you risk an
* ArrayOutOfBounds exception. See listsort.txt for a discussion of the minimum stack length required as a function of the
* length of the array being sorted and the minimum merge sequence length. */
private static final int MIN_MERGE = 32;
/** The array being sorted. */
private T[] a;
/** The comparator for this sort. */
private Comparator<? super T> c;
/** When we get into galloping mode, we stay there until both runs win less often than MIN_GALLOP consecutive times. */
private static final int MIN_GALLOP = 7;
/** This controls when we get *into* galloping mode. It is initialized to MIN_GALLOP. The mergeLo and mergeHi methods nudge it
* higher for random data, and lower for highly structured data. */
private int minGallop = MIN_GALLOP;
/** Maximum initial size of tmp array, which is used for merging. The array can grow to accommodate demand.
*
* Unlike Tim's original C version, we do not allocate this much storage when sorting smaller arrays. This change was required
* for performance. */
private static final int INITIAL_TMP_STORAGE_LENGTH = 256;
/** Temp storage for merges. */
private T[] tmp; // Actual runtime type will be Object[], regardless of T
private int tmpCount;
/** A stack of pending runs yet to be merged. Run i starts at address base[i] and extends for len[i] elements. It's always true
* (so long as the indices are in bounds) that:
*
* runBase[i] + runLen[i] == runBase[i + 1]
*
* so we could cut the storage for this, but it's a minor amount, and keeping all the info explicit simplifies the code. */
private int stackSize = 0; // Number of pending runs on stack
private final int[] runBase;
private final int[] runLen;
/** Asserts have been placed in if-statements for performance. To enable them, set this field to true and enable them in VM with
* a command line flag. If you modify this class, please do test the asserts! */
private static final boolean DEBUG = false;
TimSort () {
tmp = (T[])new Object[INITIAL_TMP_STORAGE_LENGTH];
runBase = new int[40];
runLen = new int[40];
}
public void doSort (T[] a, Comparator<T> c, int lo, int hi) {
stackSize = 0;
rangeCheck(a.length, lo, hi);
int nRemaining = hi - lo;
if (nRemaining < 2) return; // Arrays of size 0 and 1 are always sorted
// If array is small, do a "mini-TimSort" with no merges
if (nRemaining < MIN_MERGE) {
int initRunLen = countRunAndMakeAscending(a, lo, hi, c);
binarySort(a, lo, hi, lo + initRunLen, c);
return;
}
this.a = a;
this.c = c;
tmpCount = 0;
/** March over the array once, left to right, finding natural runs, extending short natural runs to minRun elements, and
* merging runs to maintain stack invariant. */
int minRun = minRunLength(nRemaining);
do {
// Identify next run
int runLen = countRunAndMakeAscending(a, lo, hi, c);
// If run is short, extend to min(minRun, nRemaining)
if (runLen < minRun) {
int force = nRemaining <= minRun ? nRemaining : minRun;
binarySort(a, lo, lo + force, lo + runLen, c);
runLen = force;
}
// Push run onto pending-run stack, and maybe merge
pushRun(lo, runLen);
mergeCollapse();
// Advance to find next run
lo += runLen;
nRemaining -= runLen;
} while (nRemaining != 0);
// Merge all remaining runs to complete sort
if (DEBUG) assert lo == hi;
mergeForceCollapse();
if (DEBUG) assert stackSize == 1;
this.a = null;
this.c = null;
T[] tmp = this.tmp;
for (int i = 0, n = tmpCount; i < n; i++)
tmp[i] = null;
}
/** Creates a TimSort instance to maintain the state of an ongoing sort.
*
* @param a the array to be sorted
* @param c the comparator to determine the order of the sort */
private TimSort (T[] a, Comparator<? super T> c) {
this.a = a;
this.c = c;
// Allocate temp storage (which may be increased later if necessary)
int len = a.length;
T[] newArray = (T[])new Object[len < 2 * INITIAL_TMP_STORAGE_LENGTH ? len >>> 1 : INITIAL_TMP_STORAGE_LENGTH];
tmp = newArray;
/*
* Allocate runs-to-be-merged stack (which cannot be expanded). The stack length requirements are described in listsort.txt.
* The C version always uses the same stack length (85), but this was measured to be too expensive when sorting "mid-sized"
* arrays (e.g., 100 elements) in Java. Therefore, we use smaller (but sufficiently large) stack lengths for smaller arrays.
* The "magic numbers" in the computation below must be changed if MIN_MERGE is decreased. See the MIN_MERGE declaration
* above for more information.
*/
int stackLen = (len < 120 ? 5 : len < 1542 ? 10 : len < 119151 ? 19 : 40);
runBase = new int[stackLen];
runLen = new int[stackLen];
}
/*
* The next two methods (which are package private and static) constitute the entire API of this class. Each of these methods
* obeys the contract of the public method with the same signature in java.util.Arrays.
*/
static <T> void sort (T[] a, Comparator<? super T> c) {
sort(a, 0, a.length, c);
}
static <T> void sort (T[] a, int lo, int hi, Comparator<? super T> c) {
if (c == null) {
Arrays.sort(a, lo, hi);
return;
}
rangeCheck(a.length, lo, hi);
int nRemaining = hi - lo;
if (nRemaining < 2) return; // Arrays of size 0 and 1 are always sorted
// If array is small, do a "mini-TimSort" with no merges
if (nRemaining < MIN_MERGE) {
int initRunLen = countRunAndMakeAscending(a, lo, hi, c);
binarySort(a, lo, hi, lo + initRunLen, c);
return;
}
/** March over the array once, left to right, finding natural runs, extending short natural runs to minRun elements, and
* merging runs to maintain stack invariant. */
TimSort<T> ts = new TimSort<T>(a, c);
int minRun = minRunLength(nRemaining);
do {
// Identify next run
int runLen = countRunAndMakeAscending(a, lo, hi, c);
// If run is short, extend to min(minRun, nRemaining)
if (runLen < minRun) {
int force = nRemaining <= minRun ? nRemaining : minRun;
binarySort(a, lo, lo + force, lo + runLen, c);
runLen = force;
}
// Push run onto pending-run stack, and maybe merge
ts.pushRun(lo, runLen);
ts.mergeCollapse();
// Advance to find next run
lo += runLen;
nRemaining -= runLen;
} while (nRemaining != 0);
// Merge all remaining runs to complete sort
if (DEBUG) assert lo == hi;
ts.mergeForceCollapse();
if (DEBUG) assert ts.stackSize == 1;
}
/** Sorts the specified portion of the specified array using a binary insertion sort. This is the best method for sorting small
* numbers of elements. It requires O(n log n) compares, but O(n^2) data movement (worst case).
*
* If the initial part of the specified range is already sorted, this method can take advantage of it: the method assumes that
* the elements from index {@code lo}, inclusive, to {@code start}, exclusive are already sorted.
*
* @param a the array in which a range is to be sorted
* @param lo the index of the first element in the range to be sorted
* @param hi the index after the last element in the range to be sorted
* @param start the index of the first element in the range that is not already known to be sorted (@code lo <= start <= hi}
* @param c comparator to used for the sort */
@SuppressWarnings("fallthrough")
private static <T> void binarySort (T[] a, int lo, int hi, int start, Comparator<? super T> c) {
if (DEBUG) assert lo <= start && start <= hi;
if (start == lo) start++;
for (; start < hi; start++) {
T pivot = a[start];
// Set left (and right) to the index where a[start] (pivot) belongs
int left = lo;
int right = start;
if (DEBUG) assert left <= right;
/*
* Invariants: pivot >= all in [lo, left). pivot < all in [right, start).
*/
while (left < right) {
int mid = (left + right) >>> 1;
if (c.compare(pivot, a[mid]) < 0)
right = mid;
else
left = mid + 1;
}
if (DEBUG) assert left == right;
/*
* The invariants still hold: pivot >= all in [lo, left) and pivot < all in [left, start), so pivot belongs at left. Note
* that if there are elements equal to pivot, left points to the first slot after them -- that's why this sort is stable.
* Slide elements over to make room for pivot.
*/
int n = start - left; // The number of elements to move
// Switch is just an optimization for arraycopy in default case
switch (n) {
case 2:
a[left + 2] = a[left + 1];
case 1:
a[left + 1] = a[left];
break;
default:
System.arraycopy(a, left, a, left + 1, n);
}
a[left] = pivot;
}
}
/** Returns the length of the run beginning at the specified position in the specified array and reverses the run if it is
* descending (ensuring that the run will always be ascending when the method returns).
*
* A run is the longest ascending sequence with:
*
* a[lo] <= a[lo + 1] <= a[lo + 2] <= ...
*
* or the longest descending sequence with:
*
* a[lo] > a[lo + 1] > a[lo + 2] > ...
*
* For its intended use in a stable mergesort, the strictness of the definition of "descending" is needed so that the call can
* safely reverse a descending sequence without violating stability.
*
* @param a the array in which a run is to be counted and possibly reversed
* @param lo index of the first element in the run
* @param hi index after the last element that may be contained in the run. It is required that @code{lo < hi}.
* @param c the comparator to used for the sort
* @return the length of the run beginning at the specified position in the specified array */
private static <T> int countRunAndMakeAscending (T[] a, int lo, int hi, Comparator<? super T> c) {
if (DEBUG) assert lo < hi;
int runHi = lo + 1;
if (runHi == hi) return 1;
// Find end of run, and reverse range if descending
if (c.compare(a[runHi++], a[lo]) < 0) { // Descending
while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0)
runHi++;
reverseRange(a, lo, runHi);
} else { // Ascending
while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0)
runHi++;
}
return runHi - lo;
}
/** Reverse the specified range of the specified array.
*
* @param a the array in which a range is to be reversed
* @param lo the index of the first element in the range to be reversed
* @param hi the index after the last element in the range to be reversed */
private static void reverseRange (Object[] a, int lo, int hi) {
hi--;
while (lo < hi) {
Object t = a[lo];
a[lo++] = a[hi];
a[hi--] = t;
}
}
/** Returns the minimum acceptable run length for an array of the specified length. Natural runs shorter than this will be
* extended with {@link #binarySort}.
*
* Roughly speaking, the computation is:
*
* If n < MIN_MERGE, return n (it's too small to bother with fancy stuff). Else if n is an exact power of 2, return
* MIN_MERGE/2. Else return an int k, MIN_MERGE/2 <= k <= MIN_MERGE, such that n/k is close to, but strictly less than, an
* exact power of 2.
*
* For the rationale, see listsort.txt.
*
* @param n the length of the array to be sorted
* @return the length of the minimum run to be merged */
private static int minRunLength (int n) {
if (DEBUG) assert n >= 0;
int r = 0; // Becomes 1 if any 1 bits are shifted off
while (n >= MIN_MERGE) {
r |= (n & 1);
n >>= 1;
}
return n + r;
}
/** Pushes the specified run onto the pending-run stack.
*
* @param runBase index of the first element in the run
* @param runLen the number of elements in the run */
private void pushRun (int runBase, int runLen) {
this.runBase[stackSize] = runBase;
this.runLen[stackSize] = runLen;
stackSize++;
}
/** Examines the stack of runs waiting to be merged and merges adjacent runs until the stack invariants are reestablished:
*
* 1. runLen[n - 2] > runLen[n - 1] + runLen[n] 2. runLen[n - 1] > runLen[n]
*
* where n is the index of the last run in runLen.
*
* This method has been formally verified to be correct after checking the last 4 runs.
* Checking for 3 runs results in an exception for large arrays.
* (Source: http://envisage-project.eu/proving-android-java-and-python-sorting-algorithm-is-broken-and-how-to-fix-it/)
*
* This method is called each time a new run is pushed onto the stack, so the invariants are guaranteed to hold for i <
* stackSize upon entry to the method. */
private void mergeCollapse () {
while (stackSize > 1) {
int n = stackSize - 2;
if ((n >= 1 && runLen[n - 1] <= runLen[n] + runLen[n + 1]) || (n >= 2 && runLen[n - 2] <= runLen[n] + runLen[n - 1])) {
if (runLen[n - 1] < runLen[n + 1]) n--;
} else if (runLen[n] > runLen[n + 1]) {
break; // Invariant is established
}
mergeAt(n);
}
}
/** Merges all runs on the stack until only one remains. This method is called once, to complete the sort. */
private void mergeForceCollapse () {
while (stackSize > 1) {
int n = stackSize - 2;
if (n > 0 && runLen[n - 1] < runLen[n + 1]) n--;
mergeAt(n);
}
}
/** Merges the two runs at stack indices i and i+1. Run i must be the penultimate or antepenultimate run on the stack. In other
* words, i must be equal to stackSize-2 or stackSize-3.
*
* @param i stack index of the first of the two runs to merge */
private void mergeAt (int i) {
if (DEBUG) assert stackSize >= 2;
if (DEBUG) assert i >= 0;
if (DEBUG) assert i == stackSize - 2 || i == stackSize - 3;
int base1 = runBase[i];
int len1 = runLen[i];
int base2 = runBase[i + 1];
int len2 = runLen[i + 1];
if (DEBUG) assert len1 > 0 && len2 > 0;
if (DEBUG) assert base1 + len1 == base2;
/*
* Record the length of the combined runs; if i is the 3rd-last run now, also slide over the last run (which isn't involved
* in this merge). The current run (i+1) goes away in any case.
*/
runLen[i] = len1 + len2;
if (i == stackSize - 3) {
runBase[i + 1] = runBase[i + 2];
runLen[i + 1] = runLen[i + 2];
}
stackSize--;
/*
* Find where the first element of run2 goes in run1. Prior elements in run1 can be ignored (because they're already in
* place).
*/
int k = gallopRight(a[base2], a, base1, len1, 0, c);
if (DEBUG) assert k >= 0;
base1 += k;
len1 -= k;
if (len1 == 0) return;
/*
* Find where the last element of run1 goes in run2. Subsequent elements in run2 can be ignored (because they're already in
* place).
*/
len2 = gallopLeft(a[base1 + len1 - 1], a, base2, len2, len2 - 1, c);
if (DEBUG) assert len2 >= 0;
if (len2 == 0) return;
// Merge remaining runs, using tmp array with min(len1, len2) elements
if (len1 <= len2)
mergeLo(base1, len1, base2, len2);
else
mergeHi(base1, len1, base2, len2);
}
/** Locates the position at which to insert the specified key into the specified sorted range; if the range contains an element
* equal to key, returns the index of the leftmost equal element.
*
* @param key the key whose insertion point to search for
* @param a the array in which to search
* @param base the index of the first element in the range
* @param len the length of the range; must be > 0
* @param hint the index at which to begin the search, 0 <= hint < n. The closer hint is to the result, the faster this method
* will run.
* @param c the comparator used to order the range, and to search
* @return the int k, 0 <= k <= n such that a[b + k - 1] < key <= a[b + k], pretending that a[b - 1] is minus infinity and a[b
* + n] is infinity. In other words, key belongs at index b + k; or in other words, the first k elements of a should
* precede key, and the last n - k should follow it. */
private static <T> int gallopLeft (T key, T[] a, int base, int len, int hint, Comparator<? super T> c) {
if (DEBUG) assert len > 0 && hint >= 0 && hint < len;
int lastOfs = 0;
int ofs = 1;
if (c.compare(key, a[base + hint]) > 0) {
// Gallop right until a[base+hint+lastOfs] < key <= a[base+hint+ofs]
int maxOfs = len - hint;
while (ofs < maxOfs && c.compare(key, a[base + hint + ofs]) > 0) {
lastOfs = ofs;
ofs = (ofs << 1) + 1;
if (ofs <= 0) // int overflow
ofs = maxOfs;
}
if (ofs > maxOfs) ofs = maxOfs;
// Make offsets relative to base
lastOfs += hint;
ofs += hint;
} else { // key <= a[base + hint]
// Gallop left until a[base+hint-ofs] < key <= a[base+hint-lastOfs]
final int maxOfs = hint + 1;
while (ofs < maxOfs && c.compare(key, a[base + hint - ofs]) <= 0) {
lastOfs = ofs;
ofs = (ofs << 1) + 1;
if (ofs <= 0) // int overflow
ofs = maxOfs;
}
if (ofs > maxOfs) ofs = maxOfs;
// Make offsets relative to base
int tmp = lastOfs;
lastOfs = hint - ofs;
ofs = hint - tmp;
}
if (DEBUG) assert -1 <= lastOfs && lastOfs < ofs && ofs <= len;
/*
* Now a[base+lastOfs] < key <= a[base+ofs], so key belongs somewhere to the right of lastOfs but no farther right than ofs.
* Do a binary search, with invariant a[base + lastOfs - 1] < key <= a[base + ofs].
*/
lastOfs++;
while (lastOfs < ofs) {
int m = lastOfs + ((ofs - lastOfs) >>> 1);
if (c.compare(key, a[base + m]) > 0)
lastOfs = m + 1; // a[base + m] < key
else
ofs = m; // key <= a[base + m]
}
if (DEBUG) assert lastOfs == ofs; // so a[base + ofs - 1] < key <= a[base + ofs]
return ofs;
}
/** Like gallopLeft, except that if the range contains an element equal to key, gallopRight returns the index after the
* rightmost equal element.
*
* @param key the key whose insertion point to search for
* @param a the array in which to search
* @param base the index of the first element in the range
* @param len the length of the range; must be > 0
* @param hint the index at which to begin the search, 0 <= hint < n. The closer hint is to the result, the faster this method
* will run.
* @param c the comparator used to order the range, and to search
* @return the int k, 0 <= k <= n such that a[b + k - 1] <= key < a[b + k] */
private static <T> int gallopRight (T key, T[] a, int base, int len, int hint, Comparator<? super T> c) {
if (DEBUG) assert len > 0 && hint >= 0 && hint < len;
int ofs = 1;
int lastOfs = 0;
if (c.compare(key, a[base + hint]) < 0) {
// Gallop left until a[b+hint - ofs] <= key < a[b+hint - lastOfs]
int maxOfs = hint + 1;
while (ofs < maxOfs && c.compare(key, a[base + hint - ofs]) < 0) {
lastOfs = ofs;
ofs = (ofs << 1) + 1;
if (ofs <= 0) // int overflow
ofs = maxOfs;
}
if (ofs > maxOfs) ofs = maxOfs;
// Make offsets relative to b
int tmp = lastOfs;
lastOfs = hint - ofs;
ofs = hint - tmp;
} else { // a[b + hint] <= key
// Gallop right until a[b+hint + lastOfs] <= key < a[b+hint + ofs]
int maxOfs = len - hint;
while (ofs < maxOfs && c.compare(key, a[base + hint + ofs]) >= 0) {
lastOfs = ofs;
ofs = (ofs << 1) + 1;
if (ofs <= 0) // int overflow
ofs = maxOfs;
}
if (ofs > maxOfs) ofs = maxOfs;
// Make offsets relative to b
lastOfs += hint;
ofs += hint;
}
if (DEBUG) assert -1 <= lastOfs && lastOfs < ofs && ofs <= len;
/*
* Now a[b + lastOfs] <= key < a[b + ofs], so key belongs somewhere to the right of lastOfs but no farther right than ofs.
* Do a binary search, with invariant a[b + lastOfs - 1] <= key < a[b + ofs].
*/
lastOfs++;
while (lastOfs < ofs) {
int m = lastOfs + ((ofs - lastOfs) >>> 1);
if (c.compare(key, a[base + m]) < 0)
ofs = m; // key < a[b + m]
else
lastOfs = m + 1; // a[b + m] <= key
}
if (DEBUG) assert lastOfs == ofs; // so a[b + ofs - 1] <= key < a[b + ofs]
return ofs;
}
/** Merges two adjacent runs in place, in a stable fashion. The first element of the first run must be greater than the first
* element of the second run (a[base1] > a[base2]), and the last element of the first run (a[base1 + len1-1]) must be greater
* than all elements of the second run.
*
* For performance, this method should be called only when len1 <= len2; its twin, mergeHi should be called if len1 >= len2.
* (Either method may be called if len1 == len2.)
*
* @param base1 index of first element in first run to be merged
* @param len1 length of first run to be merged (must be > 0)
* @param base2 index of first element in second run to be merged (must be aBase + aLen)
* @param len2 length of second run to be merged (must be > 0) */
private void mergeLo (int base1, int len1, int base2, int len2) {
if (DEBUG) assert len1 > 0 && len2 > 0 && base1 + len1 == base2;
// Copy first run into temp array
T[] a = this.a; // For performance
T[] tmp = ensureCapacity(len1);
System.arraycopy(a, base1, tmp, 0, len1);
int cursor1 = 0; // Indexes into tmp array
int cursor2 = base2; // Indexes int a
int dest = base1; // Indexes int a
// Move first element of second run and deal with degenerate cases
a[dest++] = a[cursor2++];
if (--len2 == 0) {
System.arraycopy(tmp, cursor1, a, dest, len1);
return;
}
if (len1 == 1) {
System.arraycopy(a, cursor2, a, dest, len2);
a[dest + len2] = tmp[cursor1]; // Last elt of run 1 to end of merge
return;
}
Comparator<? super T> c = this.c; // Use local variable for performance
int minGallop = this.minGallop; // " " " " "
outer:
while (true) {
int count1 = 0; // Number of times in a row that first run won
int count2 = 0; // Number of times in a row that second run won
/*
* Do the straightforward thing until (if ever) one run starts winning consistently.
*/
do {
if (DEBUG) assert len1 > 1 && len2 > 0;
if (c.compare(a[cursor2], tmp[cursor1]) < 0) {
a[dest++] = a[cursor2++];
count2++;
count1 = 0;
if (--len2 == 0) break outer;
} else {
a[dest++] = tmp[cursor1++];
count1++;
count2 = 0;
if (--len1 == 1) break outer;
}
} while ((count1 | count2) < minGallop);
/*
* One run is winning so consistently that galloping may be a huge win. So try that, and continue galloping until (if
* ever) neither run appears to be winning consistently anymore.
*/
do {
if (DEBUG) assert len1 > 1 && len2 > 0;
count1 = gallopRight(a[cursor2], tmp, cursor1, len1, 0, c);
if (count1 != 0) {
System.arraycopy(tmp, cursor1, a, dest, count1);
dest += count1;
cursor1 += count1;
len1 -= count1;
if (len1 <= 1) // len1 == 1 || len1 == 0
break outer;
}
a[dest++] = a[cursor2++];
if (--len2 == 0) break outer;
count2 = gallopLeft(tmp[cursor1], a, cursor2, len2, 0, c);
if (count2 != 0) {
System.arraycopy(a, cursor2, a, dest, count2);
dest += count2;
cursor2 += count2;
len2 -= count2;
if (len2 == 0) break outer;
}
a[dest++] = tmp[cursor1++];
if (--len1 == 1) break outer;
minGallop--;
} while (count1 >= MIN_GALLOP | count2 >= MIN_GALLOP);
if (minGallop < 0) minGallop = 0;
minGallop += 2; // Penalize for leaving gallop mode
} // End of "outer" loop
this.minGallop = minGallop < 1 ? 1 : minGallop; // Write back to field
if (len1 == 1) {
if (DEBUG) assert len2 > 0;
System.arraycopy(a, cursor2, a, dest, len2);
a[dest + len2] = tmp[cursor1]; // Last elt of run 1 to end of merge
} else if (len1 == 0) {
throw new IllegalArgumentException("Comparison method violates its general contract!");
} else {
if (DEBUG) assert len2 == 0;
if (DEBUG) assert len1 > 1;
System.arraycopy(tmp, cursor1, a, dest, len1);
}
}
/** Like mergeLo, except that this method should be called only if len1 >= len2; mergeLo should be called if len1 <= len2.
* (Either method may be called if len1 == len2.)
*
* @param base1 index of first element in first run to be merged
* @param len1 length of first run to be merged (must be > 0)
* @param base2 index of first element in second run to be merged (must be aBase + aLen)
* @param len2 length of second run to be merged (must be > 0) */
private void mergeHi (int base1, int len1, int base2, int len2) {
if (DEBUG) assert len1 > 0 && len2 > 0 && base1 + len1 == base2;
// Copy second run into temp array
T[] a = this.a; // For performance
T[] tmp = ensureCapacity(len2);
System.arraycopy(a, base2, tmp, 0, len2);
int cursor1 = base1 + len1 - 1; // Indexes into a
int cursor2 = len2 - 1; // Indexes into tmp array
int dest = base2 + len2 - 1; // Indexes into a
// Move last element of first run and deal with degenerate cases
a[dest--] = a[cursor1--];
if (--len1 == 0) {
System.arraycopy(tmp, 0, a, dest - (len2 - 1), len2);
return;
}
if (len2 == 1) {
dest -= len1;
cursor1 -= len1;
System.arraycopy(a, cursor1 + 1, a, dest + 1, len1);
a[dest] = tmp[cursor2];
return;
}
Comparator<? super T> c = this.c; // Use local variable for performance
int minGallop = this.minGallop; // " " " " "
outer:
while (true) {
int count1 = 0; // Number of times in a row that first run won
int count2 = 0; // Number of times in a row that second run won
/*
* Do the straightforward thing until (if ever) one run appears to win consistently.
*/
do {
if (DEBUG) assert len1 > 0 && len2 > 1;
if (c.compare(tmp[cursor2], a[cursor1]) < 0) {
a[dest--] = a[cursor1--];
count1++;
count2 = 0;
if (--len1 == 0) break outer;
} else {
a[dest--] = tmp[cursor2--];
count2++;
count1 = 0;
if (--len2 == 1) break outer;
}
} while ((count1 | count2) < minGallop);
/*
* One run is winning so consistently that galloping may be a huge win. So try that, and continue galloping until (if
* ever) neither run appears to be winning consistently anymore.
*/
do {
if (DEBUG) assert len1 > 0 && len2 > 1;
count1 = len1 - gallopRight(tmp[cursor2], a, base1, len1, len1 - 1, c);
if (count1 != 0) {
dest -= count1;
cursor1 -= count1;
len1 -= count1;
System.arraycopy(a, cursor1 + 1, a, dest + 1, count1);
if (len1 == 0) break outer;
}
a[dest--] = tmp[cursor2--];
if (--len2 == 1) break outer;
count2 = len2 - gallopLeft(a[cursor1], tmp, 0, len2, len2 - 1, c);
if (count2 != 0) {
dest -= count2;
cursor2 -= count2;
len2 -= count2;
System.arraycopy(tmp, cursor2 + 1, a, dest + 1, count2);
if (len2 <= 1) // len2 == 1 || len2 == 0
break outer;
}
a[dest--] = a[cursor1--];
if (--len1 == 0) break outer;
minGallop--;
} while (count1 >= MIN_GALLOP | count2 >= MIN_GALLOP);
if (minGallop < 0) minGallop = 0;
minGallop += 2; // Penalize for leaving gallop mode
} // End of "outer" loop
this.minGallop = minGallop < 1 ? 1 : minGallop; // Write back to field
if (len2 == 1) {
if (DEBUG) assert len1 > 0;
dest -= len1;
cursor1 -= len1;
System.arraycopy(a, cursor1 + 1, a, dest + 1, len1);
a[dest] = tmp[cursor2]; // Move first elt of run2 to front of merge
} else if (len2 == 0) {
throw new IllegalArgumentException("Comparison method violates its general contract!");
} else {
if (DEBUG) assert len1 == 0;
if (DEBUG) assert len2 > 0;
System.arraycopy(tmp, 0, a, dest - (len2 - 1), len2);
}
}
/** Ensures that the external array tmp has at least the specified number of elements, increasing its size if necessary. The
* size increases exponentially to ensure amortized linear time complexity.
*
* @param minCapacity the minimum required capacity of the tmp array
* @return tmp, whether or not it grew */
private T[] ensureCapacity (int minCapacity) {
tmpCount = Math.max(tmpCount, minCapacity);
if (tmp.length < minCapacity) {
// Compute smallest power of 2 > minCapacity
int newSize = minCapacity;
newSize |= newSize >> 1;
newSize |= newSize >> 2;
newSize |= newSize >> 4;
newSize |= newSize >> 8;
newSize |= newSize >> 16;
newSize++;
if (newSize < 0) // Not bloody likely!
newSize = minCapacity;
else
newSize = Math.min(newSize, a.length >>> 1);
T[] newArray = (T[])new Object[newSize];
tmp = newArray;
}
return tmp;
}
/** Checks that fromIndex and toIndex are in range, and throws an appropriate exception if they aren't.
*
* @param arrayLen the length of the array
* @param fromIndex the index of the first element of the range
* @param toIndex the index after the last element of the range
* @throws IllegalArgumentException if fromIndex > toIndex
* @throws ArrayIndexOutOfBoundsException if fromIndex < 0 or toIndex > arrayLen */
private static void rangeCheck (int arrayLen, int fromIndex, int toIndex) {
if (fromIndex > toIndex) throw new IllegalArgumentException("fromIndex(" + fromIndex + ") > toIndex(" + toIndex + ")");
if (fromIndex < 0) throw new ArrayIndexOutOfBoundsException(fromIndex);
if (toIndex > arrayLen) throw new ArrayIndexOutOfBoundsException(toIndex);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,145 @@
package dorkbox.collections.ahoCorasick
import java.util.*
/**
* Creates a Finite State Machine for very fast string matching.
*
* This is a wrapper for DoubleArrayTrie, since that class is awkward to use
*/
class FiniteStateMachine<V>(private val trie: DoubleArrayTrie<V>) {
companion object {
fun <V> build(map: Map<String, V>): FiniteStateMachine<V> {
return FiniteStateMachine(DoubleArrayTrie(map))
}
fun build(strings: List<String>): FiniteStateMachine<Boolean> {
if (strings.isEmpty()) {
throw IllegalArgumentException("strings cannot be empty")
}
val map = TreeMap<String, Boolean>()
for (key in strings) {
map[key] = java.lang.Boolean.TRUE
}
return build(map)
}
fun build(vararg strings: String): FiniteStateMachine<Boolean> {
if (strings.isEmpty()) {
throw IllegalArgumentException("strings cannot be empty")
}
val map = TreeMap<String, Boolean>()
for (key in strings) {
map[key] = java.lang.Boolean.TRUE
}
return build(map)
}
// @JvmStatic
// fun main(args: Array<String>) {
// val strings = arrayOf("khanacademy.com", "cnn.com", "google.com", "fun.reddit.com", "reddit.com")
// val keys = Arrays.asList(*strings)
// var text: String
// run {
// val map = TreeMap<String, String>()
// for (key in keys) {
// map[key] = key
// }
// val fsm: FiniteStateMachine<*> = build(map)
// text = "reddit.google.com"
// println("Searching : $text")
// println(fsm.partialMatch(text))
// println("Found: " + fsm.matches(text))
// println()
// text = "reddit.com"
// println("Searching : $text")
// println(fsm.partialMatch(text))
// println("Found: " + fsm.matches(text))
// println()
// text = "fun.reddit.com"
// println("Searching : $text")
// println(fsm.partialMatch(text))
// println("Found: " + fsm.matches(text))
// }
// println("\n\nTrying with new type\n\n")
// run {
// val fsm: FiniteStateMachine<*> = build(keys)
// text = "reddit.google.com"
// println("Searching : $text")
// println(fsm.partialMatch(text))
// println("Found: " + fsm.matches(text))
// println()
// text = "reddit.com"
// println("Searching : $text")
// println(fsm.partialMatch(text))
// println("Found: " + fsm.matches(text))
// println()
// text = "fun.reddit.com"
// println("Searching : $text")
// println(fsm.partialMatch(text))
// println("Found: " + fsm.matches(text))
// }
// println("\n\nTrying with new type\n\n")
// run {
// val fsm: FiniteStateMachine<*> = build(*strings)
// text = "reddit.google.com"
// println("Searching : $text")
// println(fsm.partialMatch(text))
// println("Found: " + fsm.matches(text))
// println()
// text = "reddit.com"
// println("Searching : $text")
// println(fsm.partialMatch(text))
// println("Found: " + fsm.matches(text))
// println()
// text = "fun.reddit.com"
// println("Searching : $text")
// println(fsm.partialMatch(text))
// println("Found: " + fsm.matches(text))
// }
// val fsm: FiniteStateMachine<*> = build(*strings)
// run {
// println("Keywords Orig: " + Arrays.toString(strings))
// println("Keywords FSM : " + Arrays.toString(fsm.getKeywords()))
// }
// }
}
/**
* @return true if this string is exactly contained. False otherwise
*/
fun matches(text: String): Boolean {
return (trie.exactMatchSearch(text) > -1)
}
/**
* Parses text and finds PARTIALLY matching results. For exact matches only it is better to use `matches`
*
* @return a list of outputs that contain matches or partial matches. The returned list will specify HOW MUCH of the text matches (A full match would be from 0 (the start), to N (the length of the text).
*/
fun partialMatch(text: String): List<DoubleArrayTrie.Hit<V>> {
return trie.parseText(text)
}
/**
* Parses text and returns true if there are PARTIALLY matching results. For exact matches only it is better to use `matches`
*
* @return true if there is a match or partial match. "fun.reddit.com" will partially match to "reddit.com"
*/
fun hasPartialMatch(text: String): Boolean {
return trie.parseText(text).isNotEmpty()
}
/**
* Returns the backing keywords IN THEIR NATURAL ORDER, in the case that you need access to the original FSM data.
*
* @return for example, if the FSM was populated with [reddit.com, cnn.com], this will return [cnn.com, reddit.com]
*/
fun getKeywords(): Array<V> {
return trie.v
}
}

View File

@ -0,0 +1,202 @@
/*
* AhoCorasickDoubleArrayTrie Project
* https://github.com/hankcs/AhoCorasickDoubleArrayTrie
*
* Copyright 2008-2018 hankcs <me@hankcs.com>
* You may modify and redistribute as long as this attribution remains.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.collections.ahoCorasick
import java.util.*
/**
*
*
* A state has the following functions
*
*
*
*
* * success; successfully transferred to another state
* * failure; if you cannot jump along the string, jump to a shallow node
* * emits; hit a pattern string
*
*
*
*
*
* The root node is slightly different. The root node has no failure function. Its "failure" refers to moving to the next state according to the string path. Other nodes have a failure state.
*
*
* @author Robert Bor
*/
class State
/**
* Construct a node with a depth of depth
*/
@JvmOverloads constructor(
/**
* The length of the pattern string is also the depth of this state
*/
/**
* Get node depth
*/
val depth: Int = 0) {
/**
* The fail function, if there is no match, jumps to this state.
*/
private var failure: State? = null
/**
* Record mode string as long as this state is reachable
*/
private var emits: MutableSet<Int>? = null
/**
* The goto table, also known as the transfer function. Move to the next state based on the next character of the string
*/
private val success = TreeMap<Char, State>()
/**
* Corresponding subscript in double array
*/
var index: Int = 0
/**
* Get the largest value
*/
val largestValueId: Int?
get() = if (emits == null || emits!!.size == 0) {
null
}
else emits!!.iterator().next()
/**
* Whether it is the termination status
*/
val isAcceptable: Boolean
get() = this.depth > 0 && this.emits != null
val states: Collection<State>
get() = this.success.values
val transitions: Collection<Char>
get() = this.success.keys
/**
* Add a matching pattern string (this state corresponds to this pattern string)
*/
fun addEmit(keyword: Int) {
if (this.emits == null) {
this.emits = TreeSet(Collections.reverseOrder())
}
this.emits!!.add(keyword)
}
/**
* Add some matching pattern strings
*/
fun addEmit(emits: Collection<Int>) {
for (emit in emits) {
addEmit(emit)
}
}
/**
* Get the pattern string represented by this node (we)
*/
fun emit(): Collection<Int> {
return this.emits ?: emptyList()
}
/**
* Get the failure status
*/
fun failure(): State? {
return this.failure
}
/**
* Set the failure status
*/
fun setFailure(failState: State,
fail: IntArray) {
this.failure = failState
fail[index] = failState.index
}
/**
* Move to the next state
*
* @param character wants to transfer by this character
* @param ignoreRootState Whether to ignore the root node, it should be true if the root node calls itself, otherwise it is false
*
* @return transfer result
*/
private fun nextState(character: Char,
ignoreRootState: Boolean): State? {
var nextState: State? = this.success[character]
if (!ignoreRootState && nextState == null && this.depth == 0) {
nextState = this
}
return nextState
}
/**
* According to the character transfer, the root node transfer failure will return itself (never return null)
*/
fun nextState(character: Char): State? {
return nextState(character, false)
}
/**
* According to character transfer, any node transfer failure will return null
*/
fun nextStateIgnoreRootState(character: Char): State? {
return nextState(character, true)
}
fun addState(character: Char): State {
var nextState = nextStateIgnoreRootState(character)
if (nextState == null) {
nextState = State(this.depth + 1)
this.success[character] = nextState
}
return nextState
}
override fun toString(): String {
val sb = StringBuilder("State{")
sb.append("depth=").append(depth)
sb.append(", ID=").append(index)
sb.append(", emits=").append(emits)
sb.append(", success=").append(success.keys)
sb.append(", failureID=").append(if (failure == null) "-1" else failure!!.index)
sb.append(", failure=").append(failure)
sb.append('}')
return sb.toString()
}
/**
* Get goto table
*/
fun getSuccess(): Map<Char, State> {
return success
}
}
/**
* Construct a node with a depth of 0
*/

View File

@ -0,0 +1,17 @@
/*
* Copyright 2021 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.collections;

View File

@ -0,0 +1,24 @@
/*
* Copyright 2021 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.collections;
/**
* Required for intellij to not complain regarding `module-info` for a multi-release jar.
* This file is completely ignored by the gradle build process
*/
public
class EmptyClass {}

View File

@ -0,0 +1,24 @@
/*
* Copyright 2021 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.collections.ahoCorasick;
/**
* Required for intellij to not complain regarding `module-info` for a multi-release jar.
* This file is completely ignored by the gradle build process
*/
public
class EmptyClass {}

6
src9/module-info.java Normal file
View File

@ -0,0 +1,6 @@
module dorkbox.collections {
exports dorkbox.collections;
exports dorkbox.collections.ahoCorasick;
requires java.base;
}