From a4c33b53d58d2baec0e9f4d232291725511f3c23 Mon Sep 17 00:00:00 2001 From: Robinson Date: Sat, 19 Mar 2022 17:07:21 +0100 Subject: [PATCH] Initial commit of split off of Utilities project --- .gitignore | 121 ++ LICENSE | 29 + LICENSE.Apachev2 | 218 ++++ README.md | 39 + build.gradle.kts | 168 +++ gradle.properties | 15 + settings.gradle.kts | 15 + src/dorkbox/collections/Array.java | 662 +++++++++++ src/dorkbox/collections/ArrayMap.java | 660 +++++++++++ src/dorkbox/collections/Bias.java | 54 + src/dorkbox/collections/BinarySearch.java | 190 +++ src/dorkbox/collections/BooleanArray.java | 363 ++++++ src/dorkbox/collections/ByteArray.java | 408 +++++++ src/dorkbox/collections/CharArray.java | 408 +++++++ .../collections/ComparableTimSort.java | 805 +++++++++++++ src/dorkbox/collections/ConcurrentEntry.java | 76 ++ .../collections/ConcurrentIterator.java | 147 +++ .../ConcurrentWeakIdentityHashMap.java | 300 +++++ src/dorkbox/collections/FloatArray.java | 424 +++++++ src/dorkbox/collections/IdentityMap.java | 812 +++++++++++++ src/dorkbox/collections/IntArray.java | 408 +++++++ src/dorkbox/collections/IntFloatMap.java | 842 ++++++++++++++ src/dorkbox/collections/IntIntMap.java | 828 +++++++++++++ src/dorkbox/collections/IntMap.java | 881 ++++++++++++++ src/dorkbox/collections/IntSet.java | 571 +++++++++ .../collections/LockFreeArrayList.java | 243 ++++ src/dorkbox/collections/LockFreeBiMap.java | 407 +++++++ src/dorkbox/collections/LockFreeHashMap.java | 269 +++++ src/dorkbox/collections/LockFreeHashSet.java | 157 +++ src/dorkbox/collections/LockFreeIntBiMap.java | 438 +++++++ src/dorkbox/collections/LockFreeIntMap.java | 218 ++++ .../collections/LockFreeIntStringMap.java | 212 ++++ .../collections/LockFreeObjectIntBiMap.java | 400 +++++++ .../collections/LockFreeObjectIntMap.java | 210 ++++ .../collections/LockFreeObjectMap.java | 200 ++++ src/dorkbox/collections/LockFreeSet.java | 220 ++++ src/dorkbox/collections/LongArray.java | 408 +++++++ src/dorkbox/collections/LongMap.java | 871 ++++++++++++++ src/dorkbox/collections/MathUtil.java | 42 + src/dorkbox/collections/ObjectFloatMap.java | 795 +++++++++++++ src/dorkbox/collections/ObjectIntMap.java | 794 +++++++++++++ src/dorkbox/collections/ObjectMap.java | 836 +++++++++++++ src/dorkbox/collections/ObjectSet.java | 589 ++++++++++ src/dorkbox/collections/OrderedMap.java | 271 +++++ src/dorkbox/collections/OrderedSet.java | 169 +++ src/dorkbox/collections/Predicate.java | 113 ++ src/dorkbox/collections/QuickSelect.java | 100 ++ src/dorkbox/collections/Select.java | 96 ++ src/dorkbox/collections/Sort.java | 66 ++ src/dorkbox/collections/TimSort.java | 840 +++++++++++++ .../ahoCorasick/DoubleArrayTrie.kt | 1036 +++++++++++++++++ .../ahoCorasick/FiniteStateMachine.kt | 145 +++ src/dorkbox/collections/ahoCorasick/State.kt | 202 ++++ src/dorkbox/collections/package-info.java | 17 + src9/dorkbox/collections/EmptyClass.java | 24 + .../collections/ahoCorasick/EmptyClass.java | 24 + src9/module-info.java | 6 + 57 files changed, 19862 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 LICENSE.Apachev2 create mode 100755 README.md create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 settings.gradle.kts create mode 100644 src/dorkbox/collections/Array.java create mode 100644 src/dorkbox/collections/ArrayMap.java create mode 100644 src/dorkbox/collections/Bias.java create mode 100644 src/dorkbox/collections/BinarySearch.java create mode 100644 src/dorkbox/collections/BooleanArray.java create mode 100644 src/dorkbox/collections/ByteArray.java create mode 100644 src/dorkbox/collections/CharArray.java create mode 100644 src/dorkbox/collections/ComparableTimSort.java create mode 100644 src/dorkbox/collections/ConcurrentEntry.java create mode 100644 src/dorkbox/collections/ConcurrentIterator.java create mode 100755 src/dorkbox/collections/ConcurrentWeakIdentityHashMap.java create mode 100644 src/dorkbox/collections/FloatArray.java create mode 100644 src/dorkbox/collections/IdentityMap.java create mode 100644 src/dorkbox/collections/IntArray.java create mode 100644 src/dorkbox/collections/IntFloatMap.java create mode 100644 src/dorkbox/collections/IntIntMap.java create mode 100644 src/dorkbox/collections/IntMap.java create mode 100644 src/dorkbox/collections/IntSet.java create mode 100644 src/dorkbox/collections/LockFreeArrayList.java create mode 100644 src/dorkbox/collections/LockFreeBiMap.java create mode 100644 src/dorkbox/collections/LockFreeHashMap.java create mode 100644 src/dorkbox/collections/LockFreeHashSet.java create mode 100644 src/dorkbox/collections/LockFreeIntBiMap.java create mode 100644 src/dorkbox/collections/LockFreeIntMap.java create mode 100644 src/dorkbox/collections/LockFreeIntStringMap.java create mode 100644 src/dorkbox/collections/LockFreeObjectIntBiMap.java create mode 100644 src/dorkbox/collections/LockFreeObjectIntMap.java create mode 100644 src/dorkbox/collections/LockFreeObjectMap.java create mode 100644 src/dorkbox/collections/LockFreeSet.java create mode 100644 src/dorkbox/collections/LongArray.java create mode 100644 src/dorkbox/collections/LongMap.java create mode 100644 src/dorkbox/collections/MathUtil.java create mode 100644 src/dorkbox/collections/ObjectFloatMap.java create mode 100644 src/dorkbox/collections/ObjectIntMap.java create mode 100644 src/dorkbox/collections/ObjectMap.java create mode 100644 src/dorkbox/collections/ObjectSet.java create mode 100644 src/dorkbox/collections/OrderedMap.java create mode 100644 src/dorkbox/collections/OrderedSet.java create mode 100644 src/dorkbox/collections/Predicate.java create mode 100644 src/dorkbox/collections/QuickSelect.java create mode 100644 src/dorkbox/collections/Select.java create mode 100644 src/dorkbox/collections/Sort.java create mode 100644 src/dorkbox/collections/TimSort.java create mode 100644 src/dorkbox/collections/ahoCorasick/DoubleArrayTrie.kt create mode 100644 src/dorkbox/collections/ahoCorasick/FiniteStateMachine.kt create mode 100644 src/dorkbox/collections/ahoCorasick/State.kt create mode 100644 src/dorkbox/collections/package-info.java create mode 100644 src9/dorkbox/collections/EmptyClass.java create mode 100644 src9/dorkbox/collections/ahoCorasick/EmptyClass.java create mode 100644 src9/module-info.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..826f7b0 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..13f3f66 --- /dev/null +++ b/LICENSE @@ -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 diff --git a/LICENSE.Apachev2 b/LICENSE.Apachev2 new file mode 100644 index 0000000..79d1b97 --- /dev/null +++ b/LICENSE.Apachev2 @@ -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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100755 index 0000000..dfaf225 --- /dev/null +++ b/README.md @@ -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 +--------- +``` + + ... + + com.dorkbox + Collections + 1.0 + + +``` + +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. diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..85f8dde --- /dev/null +++ b/build.gradle.kts @@ -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 ") + 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" + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..44dcb84 --- /dev/null +++ b/gradle.properties @@ -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 diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..cce9d6c --- /dev/null +++ b/settings.gradle.kts @@ -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. + */ diff --git a/src/dorkbox/collections/Array.java b/src/dorkbox/collections/Array.java new file mode 100644 index 0000000..d3e666a --- /dev/null +++ b/src/dorkbox/collections/Array.java @@ -0,0 +1,662 @@ +/******************************************************************************* + * Copyright 2011 LibGDX. + * Mario Zechner + * Nathan Sweet + * + * 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 implements Iterable { + /** 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 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 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 array) { + addAll(array.items, 0, array.size); + } + + public void addAll (Array 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 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 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 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 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 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 select (Predicate predicate) { + if (predicateIterable == null) + predicateIterable = new Predicate.PredicateIterable(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[] toArray (Class 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 Array of (Class arrayType) { + return new Array(arrayType); + } + + /** @see #Array(boolean, int, Class) */ + static public Array of (boolean ordered, int capacity, Class arrayType) { + return new Array(ordered, capacity, arrayType); + } + + /** @see #Array(Object[]) */ + static public Array with (T... array) { + return new Array(array); + } + + @SuppressWarnings("NullableProblems") + static public class ArrayIterator implements Iterator, Iterable { + private final Array array; + private final boolean allowRemove; + int index; + boolean valid = true; + +// ArrayIterable iterable; + + public ArrayIterator (Array array) { + this(array, true); + } + + public ArrayIterator (Array 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 iterator () { + return this; + } + } + + @SuppressWarnings({"unchecked", "NullableProblems"}) + static public class ArrayIterable implements Iterable { + private final Array array; + private final boolean allowRemove; + private ArrayIterator iterator1, iterator2; + +// java.io.StringWriter lastAcquire = new java.io.StringWriter(); + + public ArrayIterable (Array array) { + this(array, true); + } + + public ArrayIterable (Array array, boolean allowRemove) { + this.array = array; + this.allowRemove = allowRemove; + } + + @Override + public Iterator 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; + } + } +} diff --git a/src/dorkbox/collections/ArrayMap.java b/src/dorkbox/collections/ArrayMap.java new file mode 100644 index 0000000..a7e3306 --- /dev/null +++ b/src/dorkbox/collections/ArrayMap.java @@ -0,0 +1,660 @@ +/******************************************************************************* + * Copyright 2011 LibGDX. + * Mario Zechner + * Nathan Sweet + * + * 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 implements Iterable> { + 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 map) { + putAll(map, 0, map.size); + } + + public void putAll (ArrayMap 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 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> 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.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 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 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 implements Iterable>, Iterator> { + private final ArrayMap map; + Entry entry = new Entry(); + int index; + boolean valid = true; + + public Entries (ArrayMap 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> iterator () { + return this; + } + + /** Note the same entry instance is returned each time this method is called. */ + @Override + public Entry 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 implements Iterable, Iterator { + private final ArrayMap map; + int index; + boolean valid = true; + + public Values (ArrayMap 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 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 toArray () { + return new Array(true, map.values, index, map.size - index); + } + + public Array toArray (Array array) { + array.addAll(map.values, index, map.size - index); + return array; + } + } + + static public class Keys implements Iterable, Iterator { + private final ArrayMap map; + int index; + boolean valid = true; + + public Keys (ArrayMap 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 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 toArray () { + return new Array(true, map.keys, index, map.size - index); + } + + public Array toArray (Array array) { + array.addAll(map.keys, index, map.size - index); + return array; + } + } +} diff --git a/src/dorkbox/collections/Bias.java b/src/dorkbox/collections/Bias.java new file mode 100644 index 0000000..60cd332 --- /dev/null +++ b/src/dorkbox/collections/Bias.java @@ -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; +} diff --git a/src/dorkbox/collections/BinarySearch.java b/src/dorkbox/collections/BinarySearch.java new file mode 100644 index 0000000..63edf94 --- /dev/null +++ b/src/dorkbox/collections/BinarySearch.java @@ -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 Indexed, and an + * Evaluator 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) must be in order from + * low to high. 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. + *

+ * This class is not thread-safe and the size and contents of the Indexed + * should not change while a search is being performed. + * + * @author Tim Boudreau + */ +public class BinarySearch { + + private final Evaluator eval; + private final Indexed 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 eval, Indexed indexed) { + this.eval = eval; + this.indexed = indexed; + assert checkSorted(); + } + + public BinarySearch(Evaluator eval, List l) { + this (eval, new ListWrap(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 + */ + public interface Evaluator { + + public long getValue(T obj); + } + + /** + * Abstraction for list-like things which have a length and indices + * @param + */ + public interface Indexed { + + public T get(long index); + + public long size(); + } + + private static final class ListWrap implements Indexed { + + private final List l; + + ListWrap(List 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 + '}'; + } + } +} diff --git a/src/dorkbox/collections/BooleanArray.java b/src/dorkbox/collections/BooleanArray.java new file mode 100644 index 0000000..fb8a9df --- /dev/null +++ b/src/dorkbox/collections/BooleanArray.java @@ -0,0 +1,363 @@ +/******************************************************************************* + * Copyright 2011 LibGDX. + * Mario Zechner + * Nathan Sweet + * + * 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. 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); + } +} diff --git a/src/dorkbox/collections/ByteArray.java b/src/dorkbox/collections/ByteArray.java new file mode 100644 index 0000000..42dcc53 --- /dev/null +++ b/src/dorkbox/collections/ByteArray.java @@ -0,0 +1,408 @@ +/******************************************************************************* + * Copyright 2011 LibGDX. + * Mario Zechner + * Nathan Sweet + * + * 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. 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); + } +} diff --git a/src/dorkbox/collections/CharArray.java b/src/dorkbox/collections/CharArray.java new file mode 100644 index 0000000..a43ad0b --- /dev/null +++ b/src/dorkbox/collections/CharArray.java @@ -0,0 +1,408 @@ +/******************************************************************************* + * Copyright 2011 LibGDX. + * Mario Zechner + * Nathan Sweet + * + * 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. 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); + } +} diff --git a/src/dorkbox/collections/ComparableTimSort.java b/src/dorkbox/collections/ComparableTimSort.java new file mode 100644 index 0000000..6860771 --- /dev/null +++ b/src/dorkbox/collections/ComparableTimSort.java @@ -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. + * + *

+ * 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 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)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)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 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 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); + } +} diff --git a/src/dorkbox/collections/ConcurrentEntry.java b/src/dorkbox/collections/ConcurrentEntry.java new file mode 100644 index 0000000..9f28237 --- /dev/null +++ b/src/dorkbox/collections/ConcurrentEntry.java @@ -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 { + private final T value; + + private volatile ConcurrentEntry next; + private volatile ConcurrentEntry prev; + + public + ConcurrentEntry(T value, ConcurrentEntry 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 next() { + return this.next; + } + + public + void clear() { + this.next = null; + } + + + public + T getValue() { + return value; + } +} diff --git a/src/dorkbox/collections/ConcurrentIterator.java b/src/dorkbox/collections/ConcurrentIterator.java new file mode 100644 index 0000000..cb5ed2f --- /dev/null +++ b/src/dorkbox/collections/ConcurrentIterator.java @@ -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 { + /** + * 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 entries = new IdentityMap(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 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 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 head = headREF.get(this); + + if (!entries.containsKey(listener)) { + head = new ConcurrentEntry(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 concurrentEntry = entries.get(listener); + + if (concurrentEntry != null) { + ConcurrentEntry 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; + } +} diff --git a/src/dorkbox/collections/ConcurrentWeakIdentityHashMap.java b/src/dorkbox/collections/ConcurrentWeakIdentityHashMap.java new file mode 100755 index 0000000..7c14db4 --- /dev/null +++ b/src/dorkbox/collections/ConcurrentWeakIdentityHashMap.java @@ -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 + * @param + * + * @author zhanhb + */ +class ConcurrentWeakIdentityHashMap extends AbstractMap implements ConcurrentMap { + + private final ConcurrentMap, V> map; + private final ReferenceQueue queue = new ReferenceQueue<>(); + private transient Set> 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 reference; + while ((reference = queue.poll()) != null) { + map.remove(reference); + } + } + + @Override + @SuppressWarnings("NestedAssignment") + public + Set> entrySet() { + Set> 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 extends WeakReference { + + private final int hash; + + Key(T t, ReferenceQueue 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> { + + private final Iterator, V>> it; + private Map.Entry nextValue; + + Iter(Iterator, V>> it) { + this.it = it; + } + + @Override + public + boolean hasNext() { + if (nextValue != null) { + return true; + } + while (it.hasNext()) { + Map.Entry, 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 next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + Map.Entry entry = nextValue; + nextValue = null; + return entry; + } + + @Override + public + void remove() { + it.remove(); + nextValue = null; + } + + } + + + private + class EntrySet extends AbstractSet> { + + @Override + public + Iterator> 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 { + + 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()); + } + } +} diff --git a/src/dorkbox/collections/FloatArray.java b/src/dorkbox/collections/FloatArray.java new file mode 100644 index 0000000..490d4d2 --- /dev/null +++ b/src/dorkbox/collections/FloatArray.java @@ -0,0 +1,424 @@ +/******************************************************************************* + * Copyright 2011 LibGDX. + * Mario Zechner + * Nathan Sweet + * + * 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. 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); + } +} diff --git a/src/dorkbox/collections/IdentityMap.java b/src/dorkbox/collections/IdentityMap.java new file mode 100644 index 0000000..ab5753e --- /dev/null +++ b/src/dorkbox/collections/IdentityMap.java @@ -0,0 +1,812 @@ +/******************************************************************************* + * Copyright 2011 LibGDX. + * Mario Zechner + * Nathan Sweet + * + * 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.
+ *
+ * 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 implements Iterable> { + 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 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> 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 K key; + public V value; + + @Override + public String toString () { + return key + "=" + value; + } + } + + @SuppressWarnings("DuplicatedCode") + static private abstract class MapIterator implements Iterable, Iterator { + public boolean hasNext; + + final IdentityMap map; + int nextIndex, currentIndex; + boolean valid = true; + + public MapIterator (IdentityMap 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 extends MapIterator> { + private Entry entry = new Entry(); + + public Entries (IdentityMap map) { + super(map); + } + + /** Note the same entry instance is returned each time this method is called. */ + @Override + public Entry 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> iterator () { + return this; + } + } + + @SuppressWarnings({"rawtypes", "NullableProblems"}) + static public class Values extends MapIterator { + public Values (IdentityMap map) { + super((IdentityMap)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 iterator () { + return this; + } + + /** Returns a new array containing the remaining values. */ + public Array 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 array) { + while (hasNext) + array.add(next()); + } + } + + @SuppressWarnings("unchecked") + static public class Keys extends MapIterator { + public Keys (IdentityMap map) { + super((IdentityMap)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 iterator () { + return this; + } + + /** Returns a new array containing the remaining keys. */ + public Array toArray () { + Array array = new Array(true, map.size); + while (hasNext) + array.add(next()); + return array; + } + } +} diff --git a/src/dorkbox/collections/IntArray.java b/src/dorkbox/collections/IntArray.java new file mode 100644 index 0000000..6b1f801 --- /dev/null +++ b/src/dorkbox/collections/IntArray.java @@ -0,0 +1,408 @@ +/******************************************************************************* + * Copyright 2011 LibGDX. + * Mario Zechner + * Nathan Sweet + * + * 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. 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); + } +} diff --git a/src/dorkbox/collections/IntFloatMap.java b/src/dorkbox/collections/IntFloatMap.java new file mode 100644 index 0000000..e39d0f9 --- /dev/null +++ b/src/dorkbox/collections/IntFloatMap.java @@ -0,0 +1,842 @@ +/******************************************************************************* + * Copyright 2011 LibGDX. + * Mario Zechner + * Nathan Sweet + * + * 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.
+ *
+ * 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 { + 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 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, Iterator { + 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 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; + } + } +} diff --git a/src/dorkbox/collections/IntIntMap.java b/src/dorkbox/collections/IntIntMap.java new file mode 100644 index 0000000..4c63e19 --- /dev/null +++ b/src/dorkbox/collections/IntIntMap.java @@ -0,0 +1,828 @@ +/******************************************************************************* + * Copyright 2011 LibGDX. + * Mario Zechner + * Nathan Sweet + * + * 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.
+ *
+ * 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 { + 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 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, Iterator { + 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 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; + } + } +} diff --git a/src/dorkbox/collections/IntMap.java b/src/dorkbox/collections/IntMap.java new file mode 100644 index 0000000..30b50c3 --- /dev/null +++ b/src/dorkbox/collections/IntMap.java @@ -0,0 +1,881 @@ +/******************************************************************************* + * Copyright 2011 LibGDX. + * Mario Zechner + * Nathan Sweet + * + * 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.
+ *
+ * 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 implements Iterable> { + 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 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 map) { + for (Entry 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 notFound 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 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> 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 V value; + + @Override + 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 IntMap map; + int nextIndex, currentIndex; + boolean valid = true; + + public MapIterator (IntMap 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 extends MapIterator implements Iterable>, Iterator> { + private Entry 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 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> iterator () { + return this; + } + + @Override + public void remove () { + super.remove(); + } + } + + @SuppressWarnings("unchecked") + static public class Values extends MapIterator implements Iterable, Iterator { + public Values (IntMap 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 iterator () { + return this; + } + + /** Returns a new array containing the remaining values. */ + public Array 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; + } + } +} diff --git a/src/dorkbox/collections/IntSet.java b/src/dorkbox/collections/IntSet.java new file mode 100644 index 0000000..c6dcfab --- /dev/null +++ b/src/dorkbox/collections/IntSet.java @@ -0,0 +1,571 @@ +/******************************************************************************* + * Copyright 2011 LibGDX. + * Mario Zechner + * Nathan Sweet + * + * 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.
+ *
+ * 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; + } + } +} diff --git a/src/dorkbox/collections/LockFreeArrayList.java b/src/dorkbox/collections/LockFreeArrayList.java new file mode 100644 index 0000000..8645710 --- /dev/null +++ b/src/dorkbox/collections/LockFreeArrayList.java @@ -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 implements List, RandomAccess, Cloneable, Serializable { + // Recommended for best performance while adhering to the "single writer principle". Must be static-final + private static final AtomicReferenceFieldUpdater listRef = + AtomicReferenceFieldUpdater.newUpdater(LockFreeArrayList.class, + ArrayList.class, "arrayList"); + private volatile ArrayList arrayList = new ArrayList<>(); + + + public + LockFreeArrayList(){} + + + public + LockFreeArrayList(Collection 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 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 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 listIterator() { + return listRef.get(this).listIterator(); + } + + // lock-free get + @Override + public + ListIterator listIterator(final int index) { + return listRef.get(this).listIterator(index); + } + + // lock-free get + @Override + public + List 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 iterator() { + return listRef.get(this).iterator(); + } + + // lock-free get + @Override + public + Object[] toArray() { + return listRef.get(this).toArray(); + } + + // lock-free get + @Override + public + T[] toArray(final T[] targetArray) { + //noinspection unchecked + return (T[]) listRef.get(this).toArray(targetArray); + } + + // lock-free get + @SuppressWarnings("unchecked") + public + ArrayList elements() { + return listRef.get(this); + } +} diff --git a/src/dorkbox/collections/LockFreeBiMap.java b/src/dorkbox/collections/LockFreeBiMap.java new file mode 100644 index 0000000..0c2d2d8 --- /dev/null +++ b/src/dorkbox/collections/LockFreeBiMap.java @@ -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 { + // Recommended for best performance while adhering to the "single writer principle". Must be static-final + private static final AtomicReferenceFieldUpdater forwardREF = + AtomicReferenceFieldUpdater.newUpdater(LockFreeBiMap.class, + HashMap.class, + "forwardHashMap"); + + private static final AtomicReferenceFieldUpdater reverseREF = + AtomicReferenceFieldUpdater.newUpdater(LockFreeBiMap.class, + HashMap.class, + "reverseHashMap"); + + private volatile HashMap forwardHashMap; + private volatile HashMap reverseHashMap; + private final LockFreeBiMap 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(); + reverseHashMap = new HashMap(); + this.inverse = new LockFreeBiMap(reverseHashMap, forwardHashMap, this); + } + + private + LockFreeBiMap(final HashMap forwardHashMap, final HashMap reverseHashMap, final LockFreeBiMap 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 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 hashMap) throws IllegalArgumentException { + if (hashMap == null) { + throw new NullPointerException("hashMap"); + } + + LockFreeBiMap biMap = new LockFreeBiMap(); + + 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 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 key, or + * null if there was no mapping for key. + * (A null return can also indicate that the map + * previously associated null with key.) + * + * @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 key, or + * null if there was no mapping for key. + * (A null return can also indicate that the map + * previously associated null with key.) + */ + 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 hashMap) throws IllegalArgumentException { + LockFreeBiMap biMap = new LockFreeBiMap(); + + try { + for (Map.Entry 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 hashMap) { + for (Map.Entry 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 key, or + * null if there was no mapping for key. + * (A null return can also indicate that the map + * previously associated null with key.) + */ + 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. + *

+ *

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.) + *

+ *

A return value of {@code null} does not necessarily + * 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. + *

+ *

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.) + *

+ *

A return value of {@code null} does not necessarily + * 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 remove operation), + * the results of the iteration are undefined. The collection + * supports element removal, which removes the corresponding + * mapping from the map, via the Iterator.remove, + * Collection.remove, removeAll, + * retainAll and clear operations. It does not + * support the add or addAll operations. + * + * @return a view of the values contained in this map + */ + @SuppressWarnings("unchecked") + public + Collection values() { + // use the SWP to get a lock-free get of the value + return forwardREF.get(this).values(); + } + + /** + * Returns true if this bimap contains no key-value mappings. + * + * @return true 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 remove operation), + * the results of the iteration are undefined. The collection + * supports element removal, which removes the corresponding + * mapping from the map, via the Iterator.remove, + * Collection.remove, removeAll, + * retainAll and clear operations. It does not + * support the add or addAll operations. + * + * @return a view of the values contained in this map + */ + @SuppressWarnings("unchecked") + public + Collection reverseValues() { + // use the SWP to get a lock-free get of the value + return reverseREF.get(this).values(); + } +} diff --git a/src/dorkbox/collections/LockFreeHashMap.java b/src/dorkbox/collections/LockFreeHashMap.java new file mode 100644 index 0000000..0e15c1b --- /dev/null +++ b/src/dorkbox/collections/LockFreeHashMap.java @@ -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. + *

+ * 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 LockFreeHashMap implements Map, Cloneable, Serializable { + // Recommended for best performance while adhering to the "single writer principle". Must be static-final + private static final AtomicReferenceFieldUpdater mapREF = AtomicReferenceFieldUpdater.newUpdater( + LockFreeHashMap.class, + HashMap.class, + "hashMap"); + + private volatile HashMap 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 HashMap with the default initial capacity + * (16) and the default load factor (0.75). + */ + public + LockFreeHashMap() { + hashMap = new HashMap(); + } + + /** + * Constructs an empty HashMap 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(initialCapacity); + } + + /** + * Constructs a new HashMap with the same mappings as the + * specified Map. The HashMap is created with + * default load factor (0.75) and an initial capacity sufficient to + * hold the mappings in the specified Map. + * + * @param map the map whose mappings are to be placed in this map + * + * @throws NullPointerException if the specified map is null + */ + public + LockFreeHashMap(Map map) { + this.hashMap = new HashMap(map); + } + + /** + * Constructs an empty HashMap 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(initialCapacity, loadFactor); + } + + @SuppressWarnings("unchecked") + public + Map 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> iterator = hashMap.entrySet().iterator(); iterator.hasNext(); ) { + final Map.Entry kvEntry = iterator.next(); + if (kvEntry.getValue().equals(value)) { + iterator.remove(); + } + } + } + + @Override + public synchronized + void putAll(final Map 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 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 hashMap) { + this.hashMap.clear(); + this.hashMap.putAll(hashMap); + } + + @Override + public synchronized + void clear() { + hashMap.clear(); + } + + @Override + public + Set keySet() { + return getMap().keySet(); + } + + @Override + public + Collection values() { + return getMap().values(); + } + + @Override + public + Set> 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 keys() { + // use the SWP to get a lock-free get of the value + return mapREF.get(this).keySet(); + } + + @SuppressWarnings("unchecked") + public + Map elements() { + // use the SWP to get a lock-free get of the value + return mapREF.get(this); + } + + @SuppressWarnings("unchecked") + public + HashMap backingMap() { + // use the SWP to get a lock-free get of the value + return mapREF.get(this); + } +} diff --git a/src/dorkbox/collections/LockFreeHashSet.java b/src/dorkbox/collections/LockFreeHashSet.java new file mode 100644 index 0000000..c54ef3a --- /dev/null +++ b/src/dorkbox/collections/LockFreeHashSet.java @@ -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 implements Set, Cloneable, Serializable { + // Recommended for best performance while adhering to the "single writer principle". Must be static-final + private static final AtomicReferenceFieldUpdater setREF = + AtomicReferenceFieldUpdater.newUpdater(LockFreeHashSet.class, + Set.class, + "hashSet"); + private volatile Set hashSet = new HashSet<>(); + + + public + LockFreeHashSet(){} + + + public + LockFreeHashSet(Collection 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 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 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 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[] toArray(final T[] targetArray) { + //noinspection unchecked + return (T[]) setREF.get(this).toArray(targetArray); + } +} diff --git a/src/dorkbox/collections/LockFreeIntBiMap.java b/src/dorkbox/collections/LockFreeIntBiMap.java new file mode 100644 index 0000000..3c0640f --- /dev/null +++ b/src/dorkbox/collections/LockFreeIntBiMap.java @@ -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 { + // Recommended for best performance while adhering to the "single writer principle". Must be static-final + private static final AtomicReferenceFieldUpdater forwardREF = AtomicReferenceFieldUpdater.newUpdater( + LockFreeIntBiMap.class, + IntMap.class, + "forwardHashMap"); + + private static final AtomicReferenceFieldUpdater reverseREF = AtomicReferenceFieldUpdater.newUpdater( + LockFreeIntBiMap.class, + ObjectIntMap.class, + "reverseHashMap"); + + private volatile IntMap forwardHashMap; + private volatile ObjectIntMap reverseHashMap; + + private final int defaultReturnValue; + private final LockFreeObjectIntBiMap 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 test = new LockFreeIntBiMap(); + // 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(), new ObjectIntMap(), 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 forwardHashMap, ObjectIntMap reverseHashMap, int defaultReturnValue) { + this.forwardHashMap = forwardHashMap; + this.reverseHashMap = reverseHashMap; + this.defaultReturnValue = defaultReturnValue; + + this.inverse = new LockFreeObjectIntBiMap(reverseHashMap, forwardHashMap, defaultReturnValue, this); + } + + LockFreeIntBiMap(final IntMap forwardHashMap, + final ObjectIntMap reverseHashMap, + final int defaultReturnValue, + final LockFreeObjectIntBiMap 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 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 key, or + * null if there was no mapping for key. + * (A null return can also indicate that the map + * previously associated null with key.) + * + * @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 key, or + * null if there was no mapping for key. + * (A null return can also indicate that the map + * previously associated null with key.) + */ + 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 hashMap) throws IllegalArgumentException { + LockFreeIntBiMap biMap = new LockFreeIntBiMap(); + + try { + for (Map.Entry 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 hashMap) { + for (Map.Entry 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 key, or + * null if there was no mapping for key. + * (A null return can also indicate that the map + * previously associated null with key.) + */ + 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. + *

+ *

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.) + *

+ *

A return value of {@code null} does not necessarily + * 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 true if this bimap contains no key-value mappings. + * + * @return true 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 Integer.MAX_VALUE elements, returns + * Integer.MAX_VALUE. + * + * @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 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 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(); + } +} diff --git a/src/dorkbox/collections/LockFreeIntMap.java b/src/dorkbox/collections/LockFreeIntMap.java new file mode 100644 index 0000000..722a978 --- /dev/null +++ b/src/dorkbox/collections/LockFreeIntMap.java @@ -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. + *

+ * 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 + * + * 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.
+ *
+ * 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 implements Cloneable, Serializable { + // Recommended for best performance while adhering to the "single writer principle". Must be static-final + private static final AtomicReferenceFieldUpdater mapREF = AtomicReferenceFieldUpdater.newUpdater( + LockFreeIntMap.class, + IntMap.class, + "map"); + + private volatile IntMap 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 IntMap with the default initial capacity + * (16) and the default load factor (0.75). + */ + public + LockFreeIntMap() { + map = new IntMap(); + } + + /** + * Constructs an empty IntMap 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(initialCapacity); + } + + /** + * Constructs an empty IntMap 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(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 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 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(); + } +} diff --git a/src/dorkbox/collections/LockFreeIntStringMap.java b/src/dorkbox/collections/LockFreeIntStringMap.java new file mode 100644 index 0000000..0f73568 --- /dev/null +++ b/src/dorkbox/collections/LockFreeIntStringMap.java @@ -0,0 +1,212 @@ +package dorkbox.collections; + +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 + * + * 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.
+ *
+ * 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 { + private static final AtomicReferenceFieldUpdater mapREF = AtomicReferenceFieldUpdater.newUpdater( + LockFreeIntStringMap.class, + IntMap.class, + "map"); + + private volatile IntMap map; + + + public LockFreeIntStringMap() { + this.map = new IntMap<>(); + } + + /** + * Constructs an empty IntMap 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 IntMap 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 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 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 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(); + } +} diff --git a/src/dorkbox/collections/LockFreeObjectIntBiMap.java b/src/dorkbox/collections/LockFreeObjectIntBiMap.java new file mode 100644 index 0000000..ac64f4d --- /dev/null +++ b/src/dorkbox/collections/LockFreeObjectIntBiMap.java @@ -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 { + // Recommended for best performance while adhering to the "single writer principle". Must be static-final + private static final AtomicReferenceFieldUpdater forwardREF = AtomicReferenceFieldUpdater.newUpdater( + LockFreeObjectIntBiMap.class, + ObjectIntMap.class, + "forwardHashMap"); + + private static final AtomicReferenceFieldUpdater reverseREF = AtomicReferenceFieldUpdater.newUpdater( + LockFreeObjectIntBiMap.class, + IntMap.class, + "reverseHashMap"); + + private volatile ObjectIntMap forwardHashMap; + private volatile IntMap reverseHashMap; + + private final int defaultReturnValue; + private final LockFreeIntBiMap 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(), new IntMap(), 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 forwardHashMap, IntMap reverseHashMap, int defaultReturnValue) { + this.forwardHashMap = forwardHashMap; + this.reverseHashMap = reverseHashMap; + this.defaultReturnValue = defaultReturnValue; + + this.inverse = new LockFreeIntBiMap(reverseHashMap, forwardHashMap, defaultReturnValue, this); + } + + LockFreeObjectIntBiMap(final ObjectIntMap forwardHashMap, + final IntMap reverseHashMap, + final int defaultReturnValue, + final LockFreeIntBiMap 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 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 key, or + * null if there was no mapping for key. + * (A null return can also indicate that the map + * previously associated null with key.) + * + * @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 key, or + * null if there was no mapping for key. + * (A null return can also indicate that the map + * previously associated null with key.) + */ + 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 hashMap) throws IllegalArgumentException { + LockFreeObjectIntBiMap biMap = new LockFreeObjectIntBiMap(); + + try { + for (Map.Entry 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 hashMap) { + for (Map.Entry 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 key, or + * defaultReturnValue if there was no mapping for key. + * (A defaultReturnValue return can also indicate that the map + * previously associated defaultReturnValue with key.) + */ + 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. + *

+ *

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.) + *

+ *

A return value of {@code defaultReturnValue} does not necessarily + * 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 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 true if this bimap contains no key-value mappings. + * + * @return true 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 Integer.MAX_VALUE elements, returns + * Integer.MAX_VALUE. + * + * @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 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(); + } +} diff --git a/src/dorkbox/collections/LockFreeObjectIntMap.java b/src/dorkbox/collections/LockFreeObjectIntMap.java new file mode 100644 index 0000000..ad05dfd --- /dev/null +++ b/src/dorkbox/collections/LockFreeObjectIntMap.java @@ -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 { + // Recommended for best performance while adhering to the "single writer principle". Must be static-final + private static final AtomicReferenceFieldUpdater mapREF = AtomicReferenceFieldUpdater.newUpdater( + LockFreeObjectIntMap.class, + ObjectIntMap.class, + "map"); + + private volatile ObjectIntMap 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(), 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 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 hashMap) throws IllegalArgumentException { + try { + ObjectIntMap map = this.map; + for (Map.Entry 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 key, or + * defaultReturnValue if there was no mapping for key. + * (A defaultReturnValue return can also indicate that the map + * previously associated defaultReturnValue with key.) + */ + 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. + *

+ *

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.) + *

+ *

A return value of {@code defaultReturnValue} does not necessarily + * 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 true if this map contains no key-value mappings. + * + * @return true 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 Integer.MAX_VALUE elements, returns + * Integer.MAX_VALUE. + * + * @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(); + } +} diff --git a/src/dorkbox/collections/LockFreeObjectMap.java b/src/dorkbox/collections/LockFreeObjectMap.java new file mode 100644 index 0000000..e005e14 --- /dev/null +++ b/src/dorkbox/collections/LockFreeObjectMap.java @@ -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. + *

+ * 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("unchecked") +public final +class LockFreeObjectMap implements Cloneable, Serializable { + // Recommended for best performance while adhering to the "single writer principle". Must be static-final + private static final AtomicReferenceFieldUpdater mapREF = AtomicReferenceFieldUpdater.newUpdater( + LockFreeObjectMap.class, + ObjectMap.class, + "hashMap"); + + private volatile ObjectMap 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 HashMap with the default initial capacity + * (16) and the default load factor (0.75). + */ + public + LockFreeObjectMap() { + hashMap = new ObjectMap(); + } + + /** + * Constructs an empty HashMap 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(initialCapacity); + } + + /** + * Constructs an empty HashMap 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(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 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(); + } +} diff --git a/src/dorkbox/collections/LockFreeSet.java b/src/dorkbox/collections/LockFreeSet.java new file mode 100644 index 0000000..ccbc6d3 --- /dev/null +++ b/src/dorkbox/collections/LockFreeSet.java @@ -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. + *

+ * 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("unchecked") +public final +class LockFreeSet implements Set, Cloneable, java.io.Serializable { + // Recommended for best performance while adhering to the "single writer principle". Must be static-final + private static final AtomicReferenceFieldUpdater setREF = AtomicReferenceFieldUpdater.newUpdater(LockFreeSet.class, + Set.class, + "hashSet"); + + private volatile Set 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 HashMap instance has + * default initial capacity (16) and load factor (0.75). + */ + public + LockFreeSet() { + hashSet = new HashSet(); + } + + /** + * Constructs a new, empty set; the backing HashMap 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(initialCapacity, loadFactor); + } + + /** + * Constructs a new, empty set; the backing HashMap 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(initialCapacity); + } + + /** + * Constructs a new set containing the elements in the specified + * collection. The HashMap 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 collection) { + hashSet = new HashSet(collection); + } + + + @SuppressWarnings("unchecked") + public + Set 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 iterator() { + return elements().iterator(); + } + + @Override + public + Object[] toArray() { + return setREF.get(this) + .toArray(); + } + + @Override + public + 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 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(); + } +} diff --git a/src/dorkbox/collections/LongArray.java b/src/dorkbox/collections/LongArray.java new file mode 100644 index 0000000..44883b3 --- /dev/null +++ b/src/dorkbox/collections/LongArray.java @@ -0,0 +1,408 @@ +/******************************************************************************* + * Copyright 2011 LibGDX. + * Mario Zechner + * Nathan Sweet + * + * 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. 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); + } +} diff --git a/src/dorkbox/collections/LongMap.java b/src/dorkbox/collections/LongMap.java new file mode 100644 index 0000000..70f1901 --- /dev/null +++ b/src/dorkbox/collections/LongMap.java @@ -0,0 +1,871 @@ +/******************************************************************************* + * Copyright 2011 LibGDX. + * Mario Zechner + * Nathan Sweet + * + * 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.
+ *
+ * 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 implements Iterable> { + 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 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 map) { + for (Entry 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 notFound 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 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> 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 long key; + public V value; + + @Override + 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 LongMap map; + int nextIndex, currentIndex; + boolean valid = true; + + public MapIterator (LongMap 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 extends MapIterator implements Iterable>, Iterator> { + private Entry 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 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> iterator () { + return this; + } + + @Override + public void remove () { + super.remove(); + } + } + + @SuppressWarnings("rawtypes") + static public class Values extends MapIterator implements Iterable, Iterator { + public Values (LongMap 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 iterator () { + return this; + } + + /** Returns a new array containing the remaining values. */ + public Array 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; + } + } +} diff --git a/src/dorkbox/collections/MathUtil.java b/src/dorkbox/collections/MathUtil.java new file mode 100644 index 0000000..56751b6 --- /dev/null +++ b/src/dorkbox/collections/MathUtil.java @@ -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)); + } +} diff --git a/src/dorkbox/collections/ObjectFloatMap.java b/src/dorkbox/collections/ObjectFloatMap.java new file mode 100644 index 0000000..a89e872 --- /dev/null +++ b/src/dorkbox/collections/ObjectFloatMap.java @@ -0,0 +1,795 @@ +/******************************************************************************* + * Copyright 2011 LibGDX. + * Mario Zechner + * Nathan Sweet + * + * 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.
+ *
+ * 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 implements Iterable> { + 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 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 map) { + for (Entry 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 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 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 K key; + public float value; + + @Override + public String toString () { + return key + "=" + value; + } + } + + static private class MapIterator { + public boolean hasNext; + + final ObjectFloatMap map; + int nextIndex, currentIndex; + boolean valid = true; + + public MapIterator (ObjectFloatMap 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 extends MapIterator implements Iterable>, Iterator> { + private Entry entry = new Entry(); + + public Entries (ObjectFloatMap map) { + super(map); + } + + /** Note the same entry instance is returned each time this method is called. */ + @Override + public Entry 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 iterator () { + return this; + } + + @Override + public void remove () { + super.remove(); + } + } + + static public class Values extends MapIterator { + public Values (ObjectFloatMap map) { + super((ObjectFloatMap)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 extends MapIterator implements Iterable, Iterator { + public Keys (ObjectFloatMap map) { + super((ObjectFloatMap)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 iterator () { + return this; + } + + /** Returns a new array containing the remaining keys. */ + public Array toArray () { + Array array = new Array(true, map.size); + while (hasNext) + array.add(next()); + return array; + } + + /** Adds the remaining keys to the array. */ + public Array toArray (Array array) { + while (hasNext) + array.add(next()); + return array; + } + + @Override + public void remove () { + super.remove(); + } + } +} diff --git a/src/dorkbox/collections/ObjectIntMap.java b/src/dorkbox/collections/ObjectIntMap.java new file mode 100644 index 0000000..aa2d4fa --- /dev/null +++ b/src/dorkbox/collections/ObjectIntMap.java @@ -0,0 +1,794 @@ +/******************************************************************************* + * Copyright 2011 LibGDX. + * Mario Zechner + * Nathan Sweet + * + * 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.
+ *
+ * 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 implements Iterable> { + 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 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 map) { + for (Entry 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 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 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 K key; + public int value; + + @Override + public String toString () { + return key + "=" + value; + } + } + + static private class MapIterator { + public boolean hasNext; + + final ObjectIntMap map; + int nextIndex, currentIndex; + boolean valid = true; + + public MapIterator (ObjectIntMap 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 extends MapIterator implements Iterable>, Iterator> { + private Entry entry = new Entry(); + + public Entries (ObjectIntMap map) { + super(map); + } + + /** Note the same entry instance is returned each time this method is called. */ + @Override + public Entry 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 iterator () { + return this; + } + + @Override + public void remove () { + super.remove(); + } + } + + static public class Values extends MapIterator { + public Values (ObjectIntMap map) { + super((ObjectIntMap)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 extends MapIterator implements Iterable, Iterator { + public Keys (ObjectIntMap map) { + super((ObjectIntMap)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 iterator () { + return this; + } + + /** Returns a new array containing the remaining keys. */ + public Array toArray () { + Array array = new Array(true, map.size); + while (hasNext) + array.add(next()); + return array; + } + + /** Adds the remaining keys to the array. */ + public Array toArray (Array array) { + while (hasNext) + array.add(next()); + return array; + } + + @Override + public void remove () { + super.remove(); + } + } +} diff --git a/src/dorkbox/collections/ObjectMap.java b/src/dorkbox/collections/ObjectMap.java new file mode 100644 index 0000000..838fea5 --- /dev/null +++ b/src/dorkbox/collections/ObjectMap.java @@ -0,0 +1,836 @@ +/******************************************************************************* + * Copyright 2011 LibGDX. + * Mario Zechner + * Nathan Sweet + * + * 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.
+ *
+ * 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.
+ *
+ * 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 implements Iterable> { + 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 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 map) { + ensureCapacity(map.size); + for (Entry 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 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 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 Values} 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 Keys} 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 K key; + public V value; + + @Override + public String toString () { + return key + "=" + value; + } + } + + static private abstract class MapIterator implements Iterable, Iterator { + public boolean hasNext; + + final ObjectMap map; + int nextIndex, currentIndex; + boolean valid = true; + + public MapIterator (ObjectMap 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 extends MapIterator> { + Entry entry = new Entry(); + + public Entries (ObjectMap map) { + super(map); + } + + /** Note the same entry instance is returned each time this method is called. */ + @Override + public Entry 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 iterator () { + return this; + } + } + + @SuppressWarnings({"NullableProblems", "unchecked", "rawtypes"}) + static public class Values extends MapIterator { + public Values (ObjectMap map) { + super((ObjectMap)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 iterator () { + return this; + } + + /** Returns a new array containing the remaining values. */ + public Array toArray () { + return toArray(new Array(true, map.size)); + } + + /** Adds the remaining values to the specified array. */ + public Array toArray (Array array) { + while (hasNext) + array.add(next()); + return array; + } + } + + @SuppressWarnings({"unchecked", "NullableProblems", "rawtypes"}) + static public class Keys extends MapIterator { + public Keys (ObjectMap map) { + super((ObjectMap)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 iterator () { + return this; + } + + /** Returns a new array containing the remaining keys. */ + public Array toArray () { + return toArray(new Array(true, map.size)); + } + + /** Adds the remaining keys to the array. */ + public Array toArray (Array array) { + while (hasNext) + array.add(next()); + return array; + } + } +} diff --git a/src/dorkbox/collections/ObjectSet.java b/src/dorkbox/collections/ObjectSet.java new file mode 100644 index 0000000..cbe860d --- /dev/null +++ b/src/dorkbox/collections/ObjectSet.java @@ -0,0 +1,589 @@ +/******************************************************************************* + * Copyright 2011 LibGDX. + * Mario Zechner + * Nathan Sweet + * + * 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.
+ *
+ * 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.
+ *
+ * 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 implements Iterable { + 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 array) { + addAll(array.items, 0, array.size); + } + + public void addAll (Array 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 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 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 ObjectSet with (T... array) { + ObjectSet set = new ObjectSet(); + set.addAll(array); + return set; + } + + static public class ObjectSetIterator implements Iterable, Iterator { + public boolean hasNext; + + final ObjectSet set; + int nextIndex, currentIndex; + boolean valid = true; + + public ObjectSetIterator (ObjectSet 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 iterator () { + return this; + } + + /** Adds the remaining values to the array. */ + public Array toArray (Array array) { + while (hasNext) + array.add(next()); + return array; + } + + /** Returns a new array containing the remaining values. */ + public Array toArray () { + return toArray(new Array(true, set.size)); + } + } +} diff --git a/src/dorkbox/collections/OrderedMap.java b/src/dorkbox/collections/OrderedMap.java new file mode 100644 index 0000000..ecf6814 --- /dev/null +++ b/src/dorkbox/collections/OrderedMap.java @@ -0,0 +1,271 @@ +/******************************************************************************* + * Copyright 2011 LibGDX. + * Mario Zechner + * Nathan Sweet + * + * 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 extends ObjectMap { + final Array 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 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 orderedKeys () { + return keys; + } + + @Override + public Entries 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 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 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 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 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 extends Entries { + private final Array keys; + + public OrderedMapEntries (OrderedMap 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 extends Keys { + private final Array keys; + + public OrderedMapKeys (OrderedMap 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 extends Values { + private final Array keys; + + public OrderedMapValues (OrderedMap 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; + } + } +} diff --git a/src/dorkbox/collections/OrderedSet.java b/src/dorkbox/collections/OrderedSet.java new file mode 100644 index 0000000..9af5580 --- /dev/null +++ b/src/dorkbox/collections/OrderedSet.java @@ -0,0 +1,169 @@ +/******************************************************************************* + * Copyright 2011 LibGDX. + * Mario Zechner + * Nathan Sweet + * + * 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 extends ObjectSet { + final Array 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 orderedItems () { + return items; + } + + @Override + public OrderedSetIterator 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 extends ObjectSetIterator { + private Array items; + + public OrderedSetIterator (OrderedSet 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); + } + } +} diff --git a/src/dorkbox/collections/Predicate.java b/src/dorkbox/collections/Predicate.java new file mode 100644 index 0000000..4e533b4 --- /dev/null +++ b/src/dorkbox/collections/Predicate.java @@ -0,0 +1,113 @@ +/******************************************************************************* + * Copyright 2011 LibGDX. + * Mario Zechner + * Nathan Sweet + * + * 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 { + + /** @return true if the item matches the criteria and should be included in the iterator's items */ + boolean evaluate (T arg0); + + public class PredicateIterator implements Iterator { + public Iterator iterator; + public Predicate predicate; + public boolean end = false; + public boolean peeked = false; + public T next = null; + + public PredicateIterator (final Iterable iterable, final Predicate predicate) { + this(iterable.iterator(), predicate); + } + + public PredicateIterator (final Iterator iterator, final Predicate predicate) { + set(iterator, predicate); + } + + public void set (final Iterable iterable, final Predicate predicate) { + set(iterable.iterator(), predicate); + } + + public void set (final Iterator iterator, final Predicate 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 implements Iterable { + public Iterable iterable; + public Predicate predicate; + public PredicateIterator iterator = null; + + public PredicateIterable (Iterable iterable, Predicate predicate) { + set(iterable, predicate); + } + + public void set (Iterable iterable, Predicate 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 iterator () { + if (iterator == null) + iterator = new PredicateIterator(iterable.iterator(), predicate); + else + iterator.set(iterable.iterator(), predicate); + return iterator; + } + } +} diff --git a/src/dorkbox/collections/QuickSelect.java b/src/dorkbox/collections/QuickSelect.java new file mode 100644 index 0000000..53f2305 --- /dev/null +++ b/src/dorkbox/collections/QuickSelect.java @@ -0,0 +1,100 @@ +/******************************************************************************* + * Copyright 2011 LibGDX. + * Mario Zechner + * Nathan Sweet + * + * 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 { + private T[] array; + private Comparator comp; + + public int select (T[] items, Comparator 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; + } +} diff --git a/src/dorkbox/collections/Select.java b/src/dorkbox/collections/Select.java new file mode 100644 index 0000000..ad43bee --- /dev/null +++ b/src/dorkbox/collections/Select.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * Copyright 2011 LibGDX. + * Mario Zechner + * Nathan Sweet + * + * 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. + *

+ * 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} + *

+ *

+ * 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. + *

+ * @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 select (T[] items, Comparator comp, int kthLowest, int size) { + int idx = selectIndex(items, comp, kthLowest, size); + return items[idx]; + } + + public int selectIndex (T[] items, Comparator 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 int fastMin (T[] items, Comparator 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 int fastMax (T[] items, Comparator 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; + } +} diff --git a/src/dorkbox/collections/Sort.java b/src/dorkbox/collections/Sort.java new file mode 100644 index 0000000..62921f2 --- /dev/null +++ b/src/dorkbox/collections/Sort.java @@ -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).
+ *
+ * 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 void sort (Array a) { + if (comparableTimSort == null) comparableTimSort = new ComparableTimSort(); + comparableTimSort.doSort((Object[])a.items, 0, a.size); + } + + public void sort (T[] a) { + if (comparableTimSort == null) comparableTimSort = new ComparableTimSort(); + comparableTimSort.doSort(a, 0, a.length); + } + + public void sort (T[] a, int fromIndex, int toIndex) { + if (comparableTimSort == null) comparableTimSort = new ComparableTimSort(); + comparableTimSort.doSort(a, fromIndex, toIndex); + } + + public void sort (Array a, Comparator c) { + if (timSort == null) timSort = new TimSort(); + timSort.doSort((Object[])a.items, (Comparator)c, 0, a.size); + } + + public void sort (T[] a, Comparator c) { + if (timSort == null) timSort = new TimSort(); + timSort.doSort(a, c, 0, a.length); + } + + public void sort (T[] a, Comparator 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; + } +} diff --git a/src/dorkbox/collections/TimSort.java b/src/dorkbox/collections/TimSort.java new file mode 100644 index 0000000..0881bbd --- /dev/null +++ b/src/dorkbox/collections/TimSort.java @@ -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 { + /** 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 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 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 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 void sort (T[] a, Comparator c) { + sort(a, 0, a.length, c); + } + + static void sort (T[] a, int lo, int hi, Comparator 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 ts = new TimSort(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 void binarySort (T[] a, int lo, int hi, int start, Comparator 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 int countRunAndMakeAscending (T[] a, int lo, int hi, Comparator 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 int gallopLeft (T key, T[] a, int base, int len, int hint, Comparator 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 int gallopRight (T key, T[] a, int base, int len, int hint, Comparator 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 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 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); + } +} diff --git a/src/dorkbox/collections/ahoCorasick/DoubleArrayTrie.kt b/src/dorkbox/collections/ahoCorasick/DoubleArrayTrie.kt new file mode 100644 index 0000000..aa86109 --- /dev/null +++ b/src/dorkbox/collections/ahoCorasick/DoubleArrayTrie.kt @@ -0,0 +1,1036 @@ +/* + * AhoCorasickDoubleArrayTrie Project + * https://github.com/hankcs/AhoCorasickDoubleArrayTrie + * + * Copyright 2008-2018 hankcs + * 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. + */ + +@file:Suppress("unused") + +package dorkbox.collections.ahoCorasick + +import java.io.IOException +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.io.Serializable +import java.util.* + +/** + * An implementation of Aho Corasick algorithm based on Double Array Trie + * + * Will create a DoubleArray Trie from a Map or InputStream (if previously saved) + * + * @author hankcs + */ +class DoubleArrayTrie(map: Map? = null, + inputStream: ObjectInputStream? = null) : Serializable { + /** + * check array of the Double Array Trie structure + */ + private val check: IntArray + + /** + * base array of the Double Array Trie structure + */ + private val base: IntArray + + /** + * fail table of the Aho Corasick automata + */ + private val fail: IntArray + + /** + * output table of the Aho Corasick automata + */ + private val output: Array + + /** + * outer value array + */ + internal val v: Array + + /** + * the length of every key + */ + private val l: IntArray + + /** + * the size of base and check array + */ + private val size: Int + + init { + when { + map != null -> { + @Suppress("UNCHECKED_CAST") + v = kotlin.jvm.internal.collectionToArray(map.values) as Array + l = IntArray(map.size) + + val builder = Builder() + builder.build(map) + + fail = builder.fail + base = builder.base + check = builder.check + + size = builder.size + output = builder.output + } + inputStream != null -> { + @Suppress("UNCHECKED_CAST") + v = inputStream.readObject() as Array + l = inputStream.readObject() as IntArray + + fail = inputStream.readObject() as IntArray + base = inputStream.readObject() as IntArray + check = inputStream.readObject() as IntArray + size = inputStream.readObject() as Int + + @Suppress("UNCHECKED_CAST") + output = inputStream.readObject() as Array + } + else -> throw NullPointerException("Map or InputStream must be specified!") + } + } + + + /** + * Save + */ + @Throws(IOException::class) + fun save(out: ObjectOutputStream) { + out.writeObject(v) + out.writeObject(l) + out.writeObject(fail) + out.writeObject(base) + out.writeObject(check) + out.writeObject(size) + out.writeObject(output) + } + + /** + * Parse text + * + * @return a list of outputs + */ + fun parseText(text: CharSequence): List> { + var position = 1 + var currentState = 0 + val collectedEmits = LinkedList>() // unknown size, so + + for (i in 0 until text.length) { + currentState = getState(currentState, text[i]) + storeEmits(position, currentState, collectedEmits) + ++position + } + + return collectedEmits + } + + /** + * Parse text + * + * @param text The text + * @param processor A processor which handles the output + */ + fun parseText(text: CharSequence, + processor: IHit + ) { + var position = 1 + var currentState = 0 + for (i in 0 until text.length) { + currentState = getState(currentState, text[i]) + val hitArray = output[currentState] + if (hitArray != null) { + for (hit in hitArray) { + processor.hit(position - l[hit], position, v[hit]) + } + } + ++position + } + } + + /** + * Parse text + * + * @param text The text + * @param processor A processor which handles the output + */ + fun parseText(text: CharSequence, + processor: IHitCancellable + ) { + var currentState = 0 + for (i in 0 until text.length) { + val position = i + 1 + currentState = getState(currentState, text[i]) + val hitArray = output[currentState] + if (hitArray != null) { + for (hit in hitArray) { + val proceed = processor.hit(position - l[hit], position, v[hit]) + if (!proceed) { + return + } + } + } + } + } + + /** + * Parse text + * + * @param text The text + * @param processor A processor which handles the output + */ + fun parseText(text: CharArray, + processor: IHit + ) { + var position = 1 + var currentState = 0 + for (c in text) { + currentState = getState(currentState, c) + val hitArray = output[currentState] + if (hitArray != null) { + for (hit in hitArray) { + processor.hit(position - l[hit], position, v[hit]) + } + } + ++position + } + } + + /** + * Parse text + * + * @param text The text + * @param processor A processor which handles the output + */ + fun parseText(text: CharArray, + processor: IHitFull + ) { + var position = 1 + var currentState = 0 + for (c in text) { + currentState = getState(currentState, c) + val hitArray = output[currentState] + if (hitArray != null) { + for (hit in hitArray) { + processor.hit(position - l[hit], position, v[hit], hit) + } + } + ++position + } + } + + /** + * Checks that string contains at least one substring + * + * @param text source text to check + * + * @return `true` if string contains at least one substring + */ + fun matches(text: String): Boolean { + var currentState = 0 + for (i in 0 until text.length) { + currentState = getState(currentState, text[i]) + val hitArray = output[currentState] + if (hitArray != null) { + return true + } + } + return false + } + + /** + * Search first match in string + * + * @param text source text to check + * + * @return first match or `null` if there are no matches + */ + fun findFirst(text: String): Hit? { + var position = 1 + var currentState = 0 + for (i in 0 until text.length) { + currentState = getState(currentState, text[i]) + val hitArray = output[currentState] + if (hitArray != null) { + val hitIndex = hitArray[0] + return Hit(position - l[hitIndex], position, v[hitIndex]) + } + ++position + } + return null + } + + + /** + * Get value by a String key, just like a map.get() method + * + * @param key The key + */ + operator fun get(key: String): V? { + val index = exactMatchSearch(key) + return if (index >= 0) { + v[index] + } + else null + + } + + /** + * Update a value corresponding to a key + * + * @param key the key + * @param value the value + * + * @return successful or not(failure if there is no key) + */ + operator fun set(key: String, + value: V): Boolean { + val index = exactMatchSearch(key) + if (index >= 0) { + v[index] = value + return true + } + + return false + } + + /** + * Pick the value by index in value array

+ * Notice that to be more efficiently, this method DOES NOT check the parameter + * + * @param index The index + * + * @return The value + */ + operator fun get(index: Int): V { + return v[index] + } + + /** + * Processor handles the output when hit a keyword + */ + interface IHit { + /** + * Hit a keyword, you can use some code like text.substring(begin, end) to get the keyword + * + * @param begin the beginning index, inclusive. + * @param end the ending index, exclusive. + * @param value the value assigned to the keyword + */ + fun hit(begin: Int, + end: Int, + value: V) + } + + + /** + * Processor handles the output when hit a keyword, with more detail + */ + interface IHitFull { + /** + * Hit a keyword, you can use some code like text.substring(begin, end) to get the keyword + * + * @param begin the beginning index, inclusive. + * @param end the ending index, exclusive. + * @param value the value assigned to the keyword + * @param index the index of the value assigned to the keyword, you can use the integer as a perfect hash value + */ + fun hit(begin: Int, + end: Int, + value: V, + index: Int) + } + + + /** + * Callback that allows to cancel the search process. + */ + interface IHitCancellable { + /** + * Hit a keyword, you can use some code like text.substring(begin, end) to get the keyword + * + * @param begin the beginning index, inclusive. + * @param end the ending index, exclusive. + * @param value the value assigned to the keyword + * + * @return Return true for continuing the search and false for stopping it. + */ + fun hit(begin: Int, + end: Int, + value: V): Boolean + } + + + /** + * A result output + * + * @param the value type + */ + class Hit internal constructor( + /** + * the beginning index, inclusive. + */ + val begin: Int, + /** + * the ending index, exclusive. + */ + val end: Int, + /** + * the value assigned to the keyword + */ + val value: V) { + + override fun toString(): String { + return String.format("[%d:%d]=%s", begin, end, value) + } + } + + /** + * transmit state, supports failure function + */ + private fun getState(currentState: Int, + character: Char): Int { + + @Suppress("NAME_SHADOWING") + var currentState = currentState + + var newCurrentState = transitionWithRoot(currentState, character) // First press success + while (newCurrentState == -1) + // If the jump fails, press failure to jump + { + currentState = fail[currentState] + newCurrentState = transitionWithRoot(currentState, character) + } + return newCurrentState + } + + /** + * store output + */ + private fun storeEmits(position: Int, + currentState: Int, + collectedEmits: MutableList>) { + val hitArray = output[currentState] + if (hitArray != null) { + for (hit in hitArray) { + collectedEmits.add(Hit(position - l[hit], position, v[hit])) + } + } + } + + /** + * transition of a state + */ + private fun transition(current: Int, + c: Char): Int { + var b = current + var p: Int + + p = b + c.code + 1 + if (b == check[p]) { + b = base[p] + } + else { + return -1 + } + + p = b + return p + } + + /** + * transition of a state, if the state is root and it failed, then returns the root + */ + private fun transitionWithRoot(nodePos: Int, + c: Char): Int { + val b = base[nodePos] + val p: Int + + p = b + c.code + 1 + return if (b != check[p]) { + if (nodePos == 0) { + 0 + } + else -1 + } + else p + } + + /** + * match exactly by a key + * + * @param key the key + * + * @return the index of the key, you can use it as a perfect hash function + */ + fun exactMatchSearch(key: String): Int { + return exactMatchSearch(key, 0, 0, 0) + } + + /** + * match exactly by a key-char array + * + * @param keyChars the key (as a Character array) + * + * @return the index of the key, you can use it as a perfect hash function + */ + fun exactMatchSearch(keyChars: CharArray): Int { + return exactMatchSearch(keyChars, 0, 0, 0) + } + + /** + * match exactly by a key + */ + private fun exactMatchSearch(key: String, + pos: Int, + len: Int, + nodePos: Int): Int { + @Suppress("NAME_SHADOWING") + var len = len + + @Suppress("NAME_SHADOWING") + var nodePos = nodePos + + if (len <= 0) { + len = key.length + } + if (nodePos <= 0) { + nodePos = 0 + } + + var result = -1 + + val keyChars = key.toCharArray() + + var b = base[nodePos] + var p: Int + + for (i in pos until len) { + p = b + keyChars[i].code + 1 + if (b == check[p]) { + b = base[p] + } + else { + return result + } + } + + p = b + val n = base[p] + if (b == check[p] && n < 0) { + result = -n - 1 + } + return result + } + + /** + * match exactly by a key + * + * @param keyChars the char array of the key + * @param pos the begin index of char array + * @param len the length of the key + * @param nodePos the starting position of the node for searching + * + * @return the value index of the key, minus indicates null + */ + private fun exactMatchSearch(keyChars: CharArray, + pos: Int, + len: Int, + nodePos: Int): Int { + var result = -1 + + var b = base[nodePos] + var p: Int + + for (i in pos until len) { + p = b + keyChars[i].code + 1 + if (b == check[p]) { + b = base[p] + } + else { + return result + } + } + + p = b + val n = base[p] + if (b == check[p] && n < 0) { + result = -n - 1 + } + return result + } + + // /** + // * Just for debug when I wrote it + // */ + // public void debug() + // { + // System.out.println("base:"); + // for (int i = 0; i < base.length; i++) + // { + // if (base[i] < 0) + // { + // System.out.println(i + " : " + -base[i]); + // } + // } + // + // System.out.println("output:"); + // for (int i = 0; i < output.length; i++) + // { + // if (output[i] != null) + // { + // System.out.println(i + " : " + Arrays.toString(output[i])); + // } + // } + // + // System.out.println("fail:"); + // for (int i = 0; i < fail.length; i++) + // { + // if (fail[i] != 0) + // { + // System.out.println(i + " : " + fail[i]); + // } + // } + // + // System.out.println(this); + // } + // + // @Override + // public String toString() + // { + // String infoIndex = "i = "; + // String infoChar = "char = "; + // String infoBase = "base = "; + // String infoCheck = "check= "; + // for (int i = 0; i < Math.min(base.length, 200); ++i) + // { + // if (base[i] != 0 || check[i] != 0) + // { + // infoChar += " " + (i == check[i] ? " ×" : (char) (i - check[i] - 1)); + // infoIndex += " " + String.format("%5d", i); + // infoBase += " " + String.format("%5d", base[i]); + // infoCheck += " " + String.format("%5d", check[i]); + // } + // } + // return "DoubleArrayTrie:" + + // "\n" + infoChar + + // "\n" + infoIndex + + // "\n" + infoBase + + // "\n" + infoCheck + "\n" + + //// "check=" + Arrays.toString(check) + + //// ", base=" + Arrays.toString(base) + + //// ", used=" + Arrays.toString(used) + + // "size=" + size + //// ", length=" + Arrays.toString(length) + + //// ", value=" + Arrays.toString(value) + + // ; + // } + // + // /** + // * A debug class that sequentially outputs variable names and variable values + // */ + // private static class DebugArray + // { + // Map nameValueMap = new LinkedHashMap(); + // + // public void add(String name, int value) + // { + // String valueInMap = nameValueMap.get(name); + // if (valueInMap == null) + // { + // valueInMap = ""; + // } + // + // valueInMap += " " + String.format("%5d", value); + // + // nameValueMap.put(name, valueInMap); + // } + // + // @Override + // public String toString() + // { + // String text = ""; + // for (Map.Entry entry : nameValueMap.entrySet()) + // { + // String name = entry.getKey(); + // String value = entry.getValue(); + // text += String.format("%-5s", name) + "= " + value + '\n'; + // } + // + // return text; + // } + // + // public void println() + // { + // System.out.print(this); + // } + // } + + /** + * Get the size of the keywords + */ + fun size(): Int { + return v.size + } + + /** + * A builder to build the AhoCorasickDoubleArrayTrie + */ + private inner class Builder { + /** + * the root state of trie + */ + private var rootState: State? = State() + /** + * whether the position has been used + */ + private var used: BooleanArray? = null + /** + * the allocSize of the dynamic array + */ + private var allocSize: Int = 0 + /** + * a parameter controls the memory growth speed of the dynamic array + */ + private var progress: Int = 0 + /** + * the next position to check unused memory + */ + private var nextCheckPos: Int = 0 + /** + * the size of the key-pair sets + */ + private var keySize: Int = 0 + + + lateinit var output: Array + lateinit var fail: IntArray + lateinit var base: IntArray + lateinit var check: IntArray + var size: Int = 0 + + /** + * Build from a map + * + * @param map a map containing key-value pairs + */ + fun build(map: Map) { + val keySet = map.keys + + // Construct a two-point trie tree + addAllKeyword(keySet) + + // Building a double array trie tree based on a two-point trie tree + buildDoubleArrayTrie(keySet.size) + used = null + + // Build the failure table and merge the output table + constructFailureStates() + rootState = null + loseWeight() + } + + /** + * fetch siblings of a parent node + * + * @param parent parent node + * @param siblings parent node's child nodes, i . e . the siblings + * + * @return the amount of the siblings + */ + private fun fetch(parent: State, + siblings: MutableList>): Int { + + if (parent.isAcceptable) { + // This node is a child of the parent and has the output of the parent. + val fakeNode = State(-(parent.depth + 1)) + fakeNode.addEmit(parent.largestValueId!!) + siblings.add(Pair(0, fakeNode)) + } + + for ((key, value) in parent.getSuccess()) { + siblings.add(Pair(key.code + 1, value)) + } + + return siblings.size + } + + /** + * add a keyword + * + * @param keyword a keyword + * @param index the index of the keyword + */ + private fun addKeyword(keyword: String, + index: Int) { + var currentState = this.rootState + keyword.toCharArray().forEach { character -> + currentState = currentState!!.addState(character) + } + + currentState!!.addEmit(index) + l[index] = keyword.length + } + + /** + * add a collection of keywords + * + * @param keywordSet the collection holding keywords + */ + private fun addAllKeyword(keywordSet: Collection) { + var i = 0 + keywordSet.forEach { keyword -> + addKeyword(keyword, i++) + } + } + + /** + * construct failure table + */ + private fun constructFailureStates() { + fail = IntArray(Math.max(size + 1, 2)) + fail[1] = base[0] + output = arrayOfNulls(size + 1) + + val queue = ArrayDeque() + + // The first step is to set the failure of the node with depth 1 to the root node. + this.rootState!!.states.forEach { depthOneState -> + depthOneState.setFailure(this.rootState!!, fail) + queue.add(depthOneState) + constructOutput(depthOneState) + } + + // The second step is to create a failure table for nodes with depth > 1, which is a bfs + while (!queue.isEmpty()) { + val currentState = queue.remove() + + for (transition in currentState.transitions) { + val targetState = currentState.nextState(transition) + queue.add(targetState) + + var traceFailureState = currentState.failure() + while (traceFailureState!!.nextState(transition) == null) { + traceFailureState = traceFailureState.failure() + } + + val newFailureState = traceFailureState.nextState(transition) + targetState!!.setFailure(newFailureState!!, fail) + targetState.addEmit(newFailureState.emit()) + constructOutput(targetState) + } + } + } + + /** + * construct output table + */ + private fun constructOutput(targetState: State) { + val emit = targetState.emit() + if (emit.isEmpty()) { + return + } + + val output = IntArray(emit.size) + val it = emit.iterator() + for (i in output.indices) { + output[i] = it.next() + } + + this.output[targetState.index] = output + } + + private fun buildDoubleArrayTrie(keySize: Int) { + progress = 0 + this.keySize = keySize + resize(65536 * 32) // 32 double bytes + + base[0] = 1 + nextCheckPos = 0 + + val rootNode = this.rootState + val initialCapacity = rootNode!!.getSuccess().entries.size + + val siblings = ArrayList>(initialCapacity) + fetch(rootNode, siblings) + + if (siblings.isNotEmpty()) { + insert(siblings) + } + } + + /** + * allocate the memory of the dynamic array + */ + private fun resize(newSize: Int): Int { + val base2 = IntArray(newSize) + val check2 = IntArray(newSize) + val used2 = BooleanArray(newSize) + + if (allocSize > 0) { + System.arraycopy(base, 0, base2, 0, allocSize) + System.arraycopy(check, 0, check2, 0, allocSize) + System.arraycopy(used!!, 0, used2, 0, allocSize) + } + + base = base2 + check = check2 + used = used2 + + allocSize = newSize + return newSize + } + + /** + * insert the siblings to double array trie + * + * @param siblings the siblings being inserted + * + * @return the position to insert them + */ + private fun insert(siblings: List>): Int { + var begin: Int + var pos = Math.max(siblings[0].first + 1, nextCheckPos) - 1 + var nonzeroNum = 0 + var first = 0 + + if (allocSize <= pos) { + resize(pos + 1) + } + + outer@ + // The goal of this loop body is to find n free spaces that satisfy base[begin + a1...an] == 0, a1...an are n nodes in siblings + while (true) { + pos++ + + if (allocSize <= pos) { + resize(pos + 1) + } + + if (check[pos] != 0) { + nonzeroNum++ + continue + } + else if (first == 0) { + nextCheckPos = pos + first = 1 + } + + begin = pos - siblings[0].first // The distance of the current position from the first sibling node + if (allocSize <= begin + siblings[siblings.size - 1].first) { + // progress can be zero + // Prevent progress from generating zero divide errors + val l = if (1.05 > 1.0 * keySize / (progress + 1)) 1.05 else 1.0 * keySize / (progress + 1) + resize((allocSize * l).toInt()) + } + + if (used!![begin]) { + continue + } + + for (i in 1 until siblings.size) { + if (check[begin + siblings[i].first] != 0) { + continue@outer + } + } + + break + } + + // -- Simple heuristics -- + // if the percentage of non-empty contents in check between the + // index + // 'next_check_pos' and 'check' is greater than some constant value + // (e.g. 0.9), + // new 'next_check_pos' index is written by 'check'. + if (1.0 * nonzeroNum / (pos - nextCheckPos + 1) >= 0.95) { + // From the position next_check_pos to pos, if the occupied space is above 95%, the next + // time you insert a node, you can start looking directly at the pos position. + nextCheckPos = pos + } + used!![begin] = true // valid because resize is called. + + val sizeLimit = begin + siblings[siblings.size - 1].first + 1 + if (size <= sizeLimit) { + size = sizeLimit + } + + + for (sibling in siblings) { + check[begin + sibling.first] = begin + } + + for (sibling in siblings) { + val newSiblings = ArrayList>(sibling.second.getSuccess().entries.size + 1) + + if (fetch(sibling.second, newSiblings) == 0) { + // The termination of a word and not the prefix of other words, in fact, is the leaf node + base[begin + sibling.first] = 0 - sibling.second.largestValueId!! - 1 + progress++ + } + else { + val h = insert(newSiblings) // depth first search + base[begin + sibling.first] = h + } + sibling.second.index = begin + sibling.first + } + return begin + } + + /** + * free the unnecessary memory + */ + private fun loseWeight() { + base = base.copyOf(size + 65535) + check = check.copyOf(size + 65535) + } + } + +// companion object { +// @JvmStatic +// fun main(args: Array) { +// // test outliers +// test(hashMapOf()) +// +// test(hashMapOf("bmw" to "bmw")) +// +// +// var map = hashMapOf() +// var keyArray = arrayOf("bmw.com", "cnn.com", "google.com", "reddit.com") +// for (key in keyArray) { +// map[key] = key +// } +// test(map) +// +// map = hashMapOf() +// keyArray = arrayOf("bmw.com", "cnn.com", "google.com", "reddit.com", "reddit.google.com") +// for (key in keyArray) { +// map[key] = key +// } +// test(map) +// } +// +// fun test(map: Map) { +// val trie = DoubleArrayTrie(map) +// +// val text = "reddit.google.com" +// println(trie.parseText(text).toString()) +// println(trie.exactMatchSearch(text)) +// } +// } +} diff --git a/src/dorkbox/collections/ahoCorasick/FiniteStateMachine.kt b/src/dorkbox/collections/ahoCorasick/FiniteStateMachine.kt new file mode 100644 index 0000000..6d33914 --- /dev/null +++ b/src/dorkbox/collections/ahoCorasick/FiniteStateMachine.kt @@ -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(private val trie: DoubleArrayTrie) { + companion object { + fun build(map: Map): FiniteStateMachine { + return FiniteStateMachine(DoubleArrayTrie(map)) + } + + fun build(strings: List): FiniteStateMachine { + if (strings.isEmpty()) { + throw IllegalArgumentException("strings cannot be empty") + } + + val map = TreeMap() + for (key in strings) { + map[key] = java.lang.Boolean.TRUE + } + + return build(map) + } + + fun build(vararg strings: String): FiniteStateMachine { + if (strings.isEmpty()) { + throw IllegalArgumentException("strings cannot be empty") + } + + val map = TreeMap() + for (key in strings) { + map[key] = java.lang.Boolean.TRUE + } + + return build(map) + } + +// @JvmStatic +// fun main(args: Array) { +// 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() +// 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> { + 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 { + return trie.v + } +} diff --git a/src/dorkbox/collections/ahoCorasick/State.kt b/src/dorkbox/collections/ahoCorasick/State.kt new file mode 100644 index 0000000..5756bf1 --- /dev/null +++ b/src/dorkbox/collections/ahoCorasick/State.kt @@ -0,0 +1,202 @@ +/* + * AhoCorasickDoubleArrayTrie Project + * https://github.com/hankcs/AhoCorasickDoubleArrayTrie + * + * Copyright 2008-2018 hankcs + * 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? = 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() + + /** + * 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 + get() = this.success.values + + val transitions: Collection + 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) { + for (emit in emits) { + addEmit(emit) + } + } + + /** + * Get the pattern string represented by this node (we) + */ + fun emit(): Collection { + 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 { + return success + } +} +/** + * Construct a node with a depth of 0 + */ diff --git a/src/dorkbox/collections/package-info.java b/src/dorkbox/collections/package-info.java new file mode 100644 index 0000000..371947e --- /dev/null +++ b/src/dorkbox/collections/package-info.java @@ -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; diff --git a/src9/dorkbox/collections/EmptyClass.java b/src9/dorkbox/collections/EmptyClass.java new file mode 100644 index 0000000..7b8e16f --- /dev/null +++ b/src9/dorkbox/collections/EmptyClass.java @@ -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 {} diff --git a/src9/dorkbox/collections/ahoCorasick/EmptyClass.java b/src9/dorkbox/collections/ahoCorasick/EmptyClass.java new file mode 100644 index 0000000..77b79fd --- /dev/null +++ b/src9/dorkbox/collections/ahoCorasick/EmptyClass.java @@ -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 {} diff --git a/src9/module-info.java b/src9/module-info.java new file mode 100644 index 0000000..23079c0 --- /dev/null +++ b/src9/module-info.java @@ -0,0 +1,6 @@ +module dorkbox.collections { + exports dorkbox.collections; + exports dorkbox.collections.ahoCorasick; + + requires java.base; +}