diff --git a/package-lock.json b/package-lock.json
index fbf52d4b90d0f4e49c73635a1e0e111e5f3bff45..47e6bce95bb6fce12ea990a40b6cdc0d9bde06e8 100644
Binary files a/package-lock.json and b/package-lock.json differ
diff --git a/package.json b/package.json
index a024c317721b89e841d890b053bfa501c0fb2fa5..6e12b2ee17dfd788b1e1a22b3e5612844619a464 100644
--- a/package.json
+++ b/package.json
@@ -30,19 +30,19 @@
   "license": "MIT",
   "devDependencies": {
     "@types/benchmark": "^1.0.30",
-    "@types/jest": "^21.1.3",
-    "@types/node": "^8.0.41",
+    "@types/jest": "^21.1.4",
+    "@types/node": "^8.0.46",
     "benchmark": "^2.1.4",
     "download-cli": "^1.0.5",
     "jest": "^21.2.1",
     "rollup": "^0.50.0",
     "rollup-plugin-buble": "^0.16.0",
-    "rollup-plugin-commonjs": "^8.2.1",
+    "rollup-plugin-commonjs": "^8.2.4",
     "rollup-plugin-json": "^2.3.0",
     "rollup-plugin-node-resolve": "^3.0.0",
     "rollup-watch": "^4.3.1",
-    "ts-jest": "^21.1.2",
-    "tslint": "^5.7.0",
+    "ts-jest": "^21.1.3",
+    "tslint": "^5.8.0",
     "typescript": "^2.5.3",
     "uglify-js": "^3.1.4",
     "util.promisify": "^1.0.0"
diff --git a/src/structure/collections/hash-set.ts b/src/structure/collections/hash-set.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8cf25df5585d98566e939beeedf6345de4e86d55
--- /dev/null
+++ b/src/structure/collections/hash-set.ts
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2017 molio contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+interface SetLike<T> {
+    readonly size: number;
+    add(a: T): boolean;
+    has(a: T): boolean;
+}
+
+class HashSetImpl<T> implements SetLike<T> {
+    size: number = 0;
+    private byHash: { [hash: number]: T[] } = Object.create(null);
+
+    add(a: T) {
+        const hash = this.getHash(a);
+        if (this.byHash[hash]) {
+            const xs = this.byHash[hash];
+            for (const x of xs) {
+                if (this.areEqual(a, x)) return false;
+            }
+            xs[xs.length] = a;
+            this.size++;
+            return true;
+        } else {
+            this.byHash[hash] = [a];
+            this.size++;
+            return true;
+        }
+    }
+
+    has(v: T) {
+        const hash = this.getHash(v);
+        if (!this.byHash[hash]) return false;
+        for (const x of this.byHash[hash]) {
+            if (this.areEqual(v, x)) return true;
+        }
+        return false;
+    }
+
+    constructor(private getHash: (v: T) => any, private areEqual: (a: T, b: T) => boolean) { }
+}
+
+function HashSet<T>(getHash: (v: T) => any, areEqual: (a: T, b: T) => boolean): SetLike<T> {
+    return new HashSetImpl<T>(getHash, areEqual);
+}
+
+export default HashSet;
\ No newline at end of file
diff --git a/src/structure/collections/linked-index.ts b/src/structure/collections/linked-index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0f40f2d0ddafa491cf1a6ef1833c4e20a86379a3
--- /dev/null
+++ b/src/structure/collections/linked-index.ts
@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) 2017 molio contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+/** A data structure useful for graph traversal */
+interface LinkedIndex {
+    readonly head: number,
+    has(i: number): boolean,
+    remove(i: number): void
+}
+
+function LinkedIndex(size: number): LinkedIndex {
+    return new LinkedIndexImpl(size);
+}
+
+class LinkedIndexImpl implements LinkedIndex {
+    private prev: Int32Array;
+    private next: Int32Array;
+    head: number;
+
+    remove(i: number) {
+        const { prev, next } = this;
+        const p = prev[i], n = next[i];
+        if (p >= 0) {
+            next[p] = n;
+            prev[i] = -1;
+        }
+        if (n >= 0) {
+            prev[n] = p;
+            next[i] = -1;
+        }
+        if (i === this.head) {
+            if (p < 0) this.head = n;
+            else this.head = p;
+        }
+    }
+
+    has(i: number) {
+        return this.prev[i] >= 0 || this.next[i] >= 0;
+    }
+
+    constructor(size: number) {
+        this.head = size > 0 ? 0 : -1;
+        this.prev = new Int32Array(size);
+        this.next = new Int32Array(size);
+
+        for (let i = 0; i < size; i++) {
+            this.next[i] = i + 1;
+            this.prev[i] = i - 1;
+        }
+        this.prev[0] = -1;
+        this.next[size - 1] = -1;
+    }
+}
+
+export default LinkedIndex;
\ No newline at end of file
diff --git a/src/structure/collections/linked-set.ts b/src/structure/collections/linked-set.ts
deleted file mode 100644
index 9749ede223e770b6e476b697f8339c6e5ece5409..0000000000000000000000000000000000000000
--- a/src/structure/collections/linked-set.ts
+++ /dev/null
@@ -1 +0,0 @@
-// TODO: fixed length doubly linked list used for graph traversal
\ No newline at end of file
diff --git a/src/structure/collections/range-set.ts b/src/structure/collections/range-set.ts
index cd243cc378b6a39bd4e418f6470afb607ae6d5b7..6543f94287b773d374c525ec7bbcde3a4fdb86b5 100644
--- a/src/structure/collections/range-set.ts
+++ b/src/structure/collections/range-set.ts
@@ -21,6 +21,29 @@ namespace RangeSet {
         toArray(): ArrayLike<number>
     }
 
+    export function hashCode(a: RangeSet) {
+        // hash of tuple (size, min, max, mid)
+        const { size } = a;
+        let hash = 23;
+        if (!size) return hash;
+        hash = 31 * hash + size;
+        hash = 31 * hash + a.elementAt(0);
+        hash = 31 * hash + a.elementAt(size - 1);
+        if (size > 2) hash = 31 * hash + a.elementAt(size >> 1);
+        return hash;
+    }
+
+    export function areEqual(a: RangeSet, b: RangeSet) {
+        if (a === b) return true;
+        if (a instanceof RangeImpl) {
+            if (b instanceof RangeImpl) return a.min === b.min && a.max === b.max;
+            return equalAR(b as ArrayImpl, a);
+        } else if (b instanceof RangeImpl) {
+            return equalAR(a as ArrayImpl, b);
+        }
+        return equalAA(a as ArrayImpl, b as ArrayImpl);
+    }
+
     export function union(a: RangeSet, b: RangeSet) {
         if (a instanceof RangeImpl) {
             if (b instanceof RangeImpl) return unionRR(a, b);
@@ -107,6 +130,20 @@ namespace RangeSet {
         return -1;
     }
 
+    function equalAR(a: ArrayImpl, b: RangeImpl) {
+        return a.size === b.size && a.min === b.min && a.max === b.max;
+    }
+
+    function equalAA(a: ArrayImpl, b: ArrayImpl) {
+        if (a.size !== b.size || a.min !== b.min || a.max !== b.max) return false;
+        const { size, values: xs } = a;
+        const { values: ys } = b;
+        for (let i = 0; i < size; i++) {
+            if (xs[i] !== ys[i]) return false;
+        }
+        return true;
+    }
+
     function areRangesIntersecting(a: Impl, b: Impl) {
         return a.size > 0 && b.size > 0 && a.max >= b.min && a.min <= b.max;
     }
@@ -150,6 +187,8 @@ namespace RangeSet {
     function unionAA(xs: ArrayLike<number>, ys: ArrayLike<number>) {
         const la = xs.length, lb = ys.length;
 
+        // sorted list merge.
+
         let i = 0, j = 0, resultSize = 0;
         while (i < la && j < lb) {
             const x = xs[i], y = ys[j];
@@ -203,6 +242,8 @@ namespace RangeSet {
     function intersectAA(xs: ArrayLike<number>, ys: ArrayLike<number>) {
         const la = xs.length, lb = ys.length;
 
+        // a variation on sorted list merge.
+
         let i = 0, j = 0, resultSize = 0;
         while (i < la && j < lb) {
             const x = xs[i], y = ys[j];
diff --git a/src/structure/spec/collections.spec.ts b/src/structure/spec/collections.spec.ts
index 7506936f393d798f3f691265126004e69b78abb5..848ec48037aa608e95d1f276fcc220b6885427d2 100644
--- a/src/structure/spec/collections.spec.ts
+++ b/src/structure/spec/collections.spec.ts
@@ -137,6 +137,12 @@ describe('range set', () => {
     testEq('range', range, [1, 2, 3, 4]);
     testEq('sorted array', arr, [1, 3, 6]);
 
+    expect(RangeSet.areEqual(empty, singleton)).toBe(false);
+    expect(RangeSet.areEqual(singleton, singleton)).toBe(true);
+    expect(RangeSet.areEqual(range, singleton)).toBe(false);
+    expect(RangeSet.areEqual(arr, RangeSet.ofSortedArray([1, 3, 6]))).toBe(true);
+    expect(RangeSet.areEqual(arr, RangeSet.ofSortedArray([1, 4, 6]))).toBe(false);
+
     expect(empty.has(10)).toBe(false);
     expect(empty.indexOf(10)).toBe(-1);