Skip to content

Commit 9c55ce5

Browse files
committed
Changed sorting for external lists.
1 parent 7c30f6c commit 9c55ce5

7 files changed

Lines changed: 67 additions & 102 deletions

File tree

‎src/list/ext.d.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ export class ExtList<T extends object = object> extends ExtListBase<T> {
164164
reverse(): this;
165165

166166
/**
167-
* Sort nodes in place using merge sort.
167+
* Sort nodes in place using a stable natural merge sort (O(n) on already-sorted input).
168168
* @param lessFn - Comparison function returning `true` if `a` should precede `b`.
169169
* @returns `this` for chaining.
170170
*/

‎src/list/ext.js‎

Lines changed: 6 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import {ExtListBase, PtrBase} from './nodes.js';
44
import {pop, extract, splice, append} from './basics.js';
5+
import List from './core.js';
56
import {addAliases, mapIterator, normalizeIterator} from '../meta-utils.js';
67

78
export class Ptr extends PtrBase {
@@ -221,46 +222,12 @@ export class ExtList extends ExtListBase {
221222
sort(lessFn) {
222223
if (this.isOneOrEmpty) return this;
223224

224-
const left = this.make(),
225-
right = this.make();
225+
// delegate to the hosted stable natural merge sort; head lands on the sorted first node
226+
const list = List.fromExtList(this);
227+
list.sort(lessFn);
228+
this.attach(list.releaseRawList());
226229

227-
// split into two sublists
228-
let isLeft = true;
229-
for (const current of this.getNodeIterator()) {
230-
current[this.nextName] = current[this.prevName] = current; // make stand-alone
231-
if (isLeft) {
232-
left.addNodeAfter(current);
233-
left.next();
234-
} else {
235-
right.addNodeAfter(current);
236-
right.next();
237-
}
238-
isLeft = !isLeft;
239-
}
240-
this.clear();
241-
// the list is empty now
242-
243-
// sort sublists
244-
left.next().sort(lessFn);
245-
right.next().sort(lessFn);
246-
247-
// merge sublists
248-
while (!left.isEmpty && !right.isEmpty) {
249-
this.addNodeAfter((lessFn(left.head, right.head) ? left : right).removeCurrent());
250-
this.next();
251-
}
252-
if (!left.isEmpty) {
253-
const last = left.head[left.prevName];
254-
this.insertAfter(left);
255-
this.head = last;
256-
}
257-
if (!right.isEmpty) {
258-
const last = right.head[right.prevName];
259-
this.insertAfter(right);
260-
this.head = last;
261-
}
262-
263-
return this.next();
230+
return this;
264231
}
265232

266233
[Symbol.iterator]() {

‎src/slist/core.d.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,10 @@ export class SList<T extends object = object> extends HeadNode {
263263
static fromRange<T extends object = object>(range: SllRange<T> | null, options?: SllOptions): SList<T>;
264264

265265
/**
266-
* Build an SList from an external list, consuming it.
266+
* Build an SList from an external list, consuming it. By contract the result is
267+
* **rotated by one**: it starts at `extList.head`'s next node and the old head lands
268+
* last — the O(1) conversion (an order-faithful copy would cost an O(n) walk to find
269+
* the ring's back; do it via `getBack()` when order matters).
267270
* @param extList - External list to consume.
268271
* @returns A new SList.
269272
*/

‎src/slist/ext.d.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export class ExtSList<T extends object = object> extends ExtListBase<T> {
121121
reverse(): this;
122122

123123
/**
124-
* Sort nodes in place using merge sort.
124+
* Sort nodes in place using a stable natural merge sort (O(n) on already-sorted input).
125125
* @param lessFn - Returns `true` if `a` should precede `b`.
126126
* @returns `this` for chaining.
127127
*/

‎src/slist/ext.js‎

Lines changed: 12 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// @ts-self-types="./ext.d.ts"
22

33
import {ExtListBase, PtrBase} from './nodes.js';
4-
import {pop, extract, splice} from './basics.js';
4+
import {pop, extract, splice, append} from './basics.js';
5+
import SList from './core.js';
56
import {addAliases, normalizeIterator} from '../meta-utils.js';
67

78
export class Ptr extends PtrBase {
@@ -171,66 +172,18 @@ export class ExtSList extends ExtListBase {
171172
sort(lessFn) {
172173
if (this.isOneOrEmpty) return this;
173174

174-
const leftHead = {},
175-
rightHead = {};
176-
leftHead[this.nextName] = leftHead;
177-
rightHead[this.nextName] = rightHead;
178-
179-
const left = this.make(leftHead),
180-
right = this.make(rightHead);
181-
182-
// split into two sublists
183-
let isLeft = true;
184-
for (const current of this.getNodeIterator()) {
185-
current[this.nextName] = current; // make stand-alone
186-
if (isLeft) {
187-
left.addNodeAfter(current);
188-
left.next();
189-
} else {
190-
right.addNodeAfter(current);
191-
right.next();
192-
}
193-
isLeft = !isLeft;
194-
}
195-
left.removeNodeAfter(); // remove the head node
196-
right.removeNodeAfter(); // remove the head node
175+
// delegate to the hosted stable natural merge sort; head lands on the sorted first node
176+
// (SList.fromExtList is unsuitable: the SLL ext range starts at head.next — a rotation
177+
// that would break stability relative to the original order)
178+
const list = new SList(this),
179+
back = this.getBack();
180+
append(list, list, {prevFrom: back, to: back});
181+
list.last = back;
197182
this.clear();
198-
// the list is empty now
199-
200-
// sort sublists
201-
left.next().sort(lessFn);
202-
right.next().sort(lessFn);
203-
204-
// merge sublists
205-
const leftIterator = left.getNodeIterator()[Symbol.iterator](),
206-
rightIterator = right.getNodeIterator()[Symbol.iterator]();
207-
let leftItem = leftIterator.next(),
208-
rightItem = rightIterator.next();
209-
while (!leftItem.done && !rightItem.done) {
210-
let node;
211-
if (lessFn(leftItem.value, rightItem.value)) {
212-
node = leftItem.value;
213-
leftItem = leftIterator.next();
214-
} else {
215-
node = rightItem.value;
216-
rightItem = rightIterator.next();
217-
}
218-
node[this.nextName] = node; // make stand-alone
219-
this.addNodeAfter(node);
220-
this.next();
221-
}
222-
for (; !leftItem.done; this.next(), leftItem = leftIterator.next()) {
223-
const node = leftItem.value;
224-
node[this.nextName] = node; // make stand-alone
225-
this.addNodeAfter(node);
226-
}
227-
for (; !rightItem.done; this.next(), rightItem = rightIterator.next()) {
228-
const node = rightItem.value;
229-
node[this.nextName] = node; // make stand-alone
230-
this.addNodeAfter(node);
231-
}
183+
list.sort(lessFn);
184+
this.attach(list.releaseRawList());
232185

233-
return this.next();
186+
return this;
234187
}
235188

236189
[Symbol.iterator]() {

‎tests/test-sorted-ops.js‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import List from 'list-toolkit/list.js';
33
import SList from 'list-toolkit/slist.js';
44
import ValueList from 'list-toolkit/value-list.js';
55
import ValueSList from 'list-toolkit/value-slist.js';
6+
import ExtList from 'list-toolkit/ext-list.js';
7+
import ExtSList from 'list-toolkit/ext-slist.js';
68

79
const makeLcg =
810
(seed = 42) =>
@@ -123,6 +125,46 @@ test('sort - correctness across shapes', t => {
123125
}
124126
});
125127

128+
test('sort - ext lists parity and stability', t => {
129+
const random = makeLcg(11);
130+
{
131+
const items = Array.from({length: 100}, (_, i) => ({k: Math.floor(random() * 10), i})),
132+
ext = new ExtList(List.from(items).releaseRawList());
133+
ext.sort(lessNode);
134+
const result = Array.from(ext.getNodeIterator());
135+
t.equal(result.length, 100, 'ExtList: all nodes present');
136+
t.equal(ext.head, result[0], 'ExtList: head is the sorted first node');
137+
let stable = true;
138+
for (let i = 1; i < result.length; ++i) {
139+
const prev = result[i - 1],
140+
curr = result[i];
141+
if (prev.k > curr.k || (prev.k === curr.k && prev.i > curr.i)) {
142+
stable = false;
143+
break;
144+
}
145+
}
146+
t.ok(stable, 'ExtList: sorted and stable');
147+
}
148+
{
149+
const items = Array.from({length: 100}, (_, i) => ({k: Math.floor(random() * 10), i})),
150+
ext = new ExtSList(SList.from(items).releaseRawList());
151+
ext.sort(lessNode);
152+
const result = Array.from(ext.getNodeIterator());
153+
t.equal(result.length, 100, 'ExtSList: all nodes present');
154+
t.equal(ext.head, result[0], 'ExtSList: head is the sorted first node');
155+
let stable = true;
156+
for (let i = 1; i < result.length; ++i) {
157+
const prev = result[i - 1],
158+
curr = result[i];
159+
if (prev.k > curr.k || (prev.k === curr.k && prev.i > curr.i)) {
160+
stable = false;
161+
break;
162+
}
163+
}
164+
t.ok(stable, 'ExtSList: sorted and stable');
165+
}
166+
});
167+
126168
test('sort - stability', t => {
127169
const random = makeLcg(7);
128170
for (const Ctor of [List, SList]) {

‎wiki‎

Submodule wiki updated from 33412e2 to 825aead

0 commit comments

Comments
 (0)