Skip to content

Commit

Permalink
feat(dcons): add sort(), update shuffle(), add tests
Browse files Browse the repository at this point in the history
- add @thi.ng/random dep
- rewrite & optimize shuffle() w/ support for IRandom
- update readme
  • Loading branch information
postspectacular committed Nov 24, 2019
1 parent bc4141e commit f6bbcd5
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 15 deletions.
9 changes: 6 additions & 3 deletions packages/dcons/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ Comprehensive doubly linked list structure with:
- Node swaps (O(1))
- Reversing (O(n/2))
- Rotation (left / right) (O(1))
- Shuffling
- Shuffling (configurable, support custom PRNG)
- Sorting (Merge sort, w/ custom comparator)
- Slicing (sublist copies)
- Splicing (delete and/or insert)
- `release()` (emptying, GC friendly)
Expand All @@ -41,6 +42,7 @@ yarn add @thi.ng/dcons
- [@thi.ng/compare](https://github.com/thi-ng/umbrella/tree/master/packages/compare)
- [@thi.ng/equiv](https://github.com/thi-ng/umbrella/tree/master/packages/equiv)
- [@thi.ng/errors](https://github.com/thi-ng/umbrella/tree/master/packages/errors)
- [@thi.ng/random](https://github.com/thi-ng/umbrella/tree/master/packages/random)
- [@thi.ng/transducers](https://github.com/thi-ng/umbrella/tree/master/packages/transducers)

## Usage
Expand All @@ -50,13 +52,13 @@ yarn add @thi.ng/dcons
DCons = require("@thi.ng/dcons").DCons;

// ES6 / TS
import { DCons } from "@thi.ng/dcons";
import { dcons } from "@thi.ng/dcons";
```

## API

```ts
list = new DCons([1, 2, 3]);
list = dcons([1, 2, 3]);
list.length
[...list]
```
Expand Down Expand Up @@ -103,6 +105,7 @@ list.length
- `splice()`
- `swap()`
- `shuffle()`
- `sort()`
- `reverse()`
- `rotateLeft()`
- `rotateRight()`
Expand Down
1 change: 1 addition & 0 deletions packages/dcons/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@thi.ng/compare": "^1.0.10",
"@thi.ng/equiv": "^1.0.10",
"@thi.ng/errors": "^1.2.1",
"@thi.ng/random": "^1.1.13",
"@thi.ng/transducers": "^6.0.0"
},
"keywords": [
Expand Down
100 changes: 92 additions & 8 deletions packages/dcons/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { isArrayLike } from "@thi.ng/checks";
import { compare } from "@thi.ng/compare";
import { equiv } from "@thi.ng/equiv";
import { illegalArgs } from "@thi.ng/errors";
import { IRandom, SYSTEM } from "@thi.ng/random";
import { IReducible, isReduced, ReductionFn } from "@thi.ng/transducers";

export interface ConsCell<T> {
Expand Down Expand Up @@ -489,18 +490,101 @@ export class DCons<T>
return acc;
}

shuffle() {
let n = this._length;
let cell = this.tail;
while (n > 1) {
let i = Math.floor(Math.random() * n);
this.swap(this.nthCell(i)!, cell!);
cell = cell!.prev;
n--;
/**
* Shuffles list by probabilistically moving cells to head or tail
* positions.
*
* @remarks
* Supports configurable iterations and custom PRNG via
* {@link @thi.ng/random#IRandom} (default:
* {@link @thi.ng/random#SYSTEM}).
*
* Default iterations: `ceil(3/2 * log2(n))`
*
* @param iter
* @param rnd
*/
shuffle(iter?: number, rnd: IRandom = SYSTEM) {
if (this._length < 2) return this;
for (
iter = iter ?? Math.ceil(1.5 * Math.log2(this._length));
iter > 0;
iter--
) {
let cell = this.head;
while (cell) {
const next = cell.next;
rnd.float() < 0.5 ? this.asHead(cell) : this.asTail(cell);
cell = next;
}
}
return this;
}

/**
* Merge sort implementation based on Simon Tatham's algorithm:
* https://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html
*
* @remarks
* Uses {@link @thi.ng/compare#compare} as default comparator.
*
* @param cmp
*/
sort(cmp: Comparator<T> = compare) {
if (!this._length) return this;
let inSize = 1;
while (true) {
let p = this.head;
this.head = undefined;
this.tail = undefined;
let numMerges = 0;
while (p) {
numMerges++;
let q: ConsCell<T> | undefined = p;
let psize = 0;
for (let i = 0; i < inSize; i++) {
psize++;
q = q!.next;
if (!q) break;
}
let qsize = inSize;
while (psize > 0 || (qsize > 0 && q)) {
let e: ConsCell<T> | undefined;
if (psize === 0) {
e = q;
q = q!.next;
qsize--;
} else if (!q || qsize === 0) {
e = p;
p = p!.next;
psize--;
} else if (cmp(p!.value, q!.value) <= 0) {
e = p;
p = p!.next;
psize--;
} else {
e = q;
q = q!.next;
qsize--;
}
if (this.tail) {
this.tail!.next = e;
} else {
this.head = e;
}
e!.prev = this.tail;
this.tail = e;
}
p = q;
}
this.tail!.next = undefined;
if (numMerges <= 1) {
return this;
}
inSize *= 2;
}
}

reverse() {
let head = this.head;
let tail = this.tail;
Expand Down
38 changes: 34 additions & 4 deletions packages/dcons/test/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { compareNumDesc } from "@thi.ng/compare";
import { XsAdd } from "@thi.ng/random";
import { range } from "@thi.ng/transducers";
import * as assert from "assert";
import { DCons } from "../src/index";
import { DCons, dcons } from "../src/index";

describe("DCons", () => {
let a: DCons<any>, src: number[];
beforeEach(() => {
src = [1, 2, 3, 4, 5];
a = new DCons(src);
a = dcons(src);
});

it("is instanceof", () => {
Expand All @@ -14,7 +17,7 @@ describe("DCons", () => {

it("has length", () => {
assert.equal(a.length, 5);
a = new DCons();
a = dcons();
assert.equal(a.length, 0);
});

Expand All @@ -37,10 +40,37 @@ describe("DCons", () => {
assert.equal(a.seq(2, 3)!.next(), undefined);
});

it("shuffle", () => {
assert.deepEqual(
[...a.shuffle(undefined, new XsAdd(0x12345678))],
[3, 5, 1, 4, 2]
);
assert.deepEqual(
[...dcons(range(10)).shuffle(undefined, new XsAdd(0x12345678))],
[3, 0, 7, 8, 5, 2, 9, 1, 6, 4]
);
assert.deepEqual([...dcons().shuffle()], []);
assert.deepEqual([...dcons([1]).shuffle()], [1]);
});

it("sort", () => {
assert.deepEqual([...dcons().sort()], []);
assert.deepEqual([...dcons([1]).sort()], [1]);
assert.deepEqual([...dcons([1, -1]).sort()], [-1, 1]);
assert.deepEqual(
[...dcons([8, -1, 17, 5, 8, 3, 11]).sort()],
[-1, 3, 5, 8, 8, 11, 17]
);
assert.deepEqual(
[...dcons([8, -1, 17, 5, 8, 3, 11]).sort(compareNumDesc)],
[17, 11, 8, 8, 5, 3, -1]
);
});

it("works as stack", () => {
assert.equal(a.push(10).pop(), 10);
assert.equal(a.pop(), 5);
a = new DCons();
a = dcons();
assert.equal(a.pop(), undefined);
});

Expand Down

0 comments on commit f6bbcd5

Please sign in to comment.