Skip to content

Commit 473f1c3

Browse files
committed
feat(Heap, HeapAsync): implement element eviction based on limit and add _worstIndex method
Enhance the add method to reject elements worse than the worst kept when the heap reaches its limit, and replace the worst element if a better one is added. New private method, _worstIndex, to find the index of the worst element for eviction. Update _applyLimit to remove the worst elements while maintaining heap properties. Add tests to validate the new functionality for both Heap and HeapAsync. Should fix #656
1 parent e7ad714 commit 473f1c3

File tree

6 files changed

+225
-21
lines changed

6 files changed

+225
-21
lines changed

src/Heap.ts

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -352,11 +352,21 @@ export class Heap<T> implements Iterable<T> {
352352
* Adds an element to the heap. Aliases: {@link offer}.
353353
* Same as: {@link push}(element).
354354
* @param {any} element Element to be added
355-
* @return {Boolean} true
355+
* @return {Boolean} true if added, false if limit exceeded and element not good enough
356356
*/
357357
add(element: T): boolean {
358+
if (this._limit > 0 && this.heapArray.length >= this._limit) {
359+
const worstIdx = this._worstIndex();
360+
if (this.compare(element, this.heapArray[worstIdx]) >= 0) {
361+
return false; // New element is not better than worst keeper
362+
}
363+
// Replace worst with new element
364+
this.heapArray[worstIdx] = element;
365+
this._sortNodeUp(worstIdx);
366+
this._sortNodeDown(worstIdx);
367+
return true;
368+
}
358369
this._sortNodeUp(this.heapArray.push(element) - 1);
359-
this._applyLimit();
360370
return true;
361371
}
362372

@@ -819,14 +829,21 @@ export class Heap<T> implements Iterable<T> {
819829
}
820830

821831
/**
822-
* Limit heap size if needed
832+
* Limit heap size if needed, removing worst elements to keep best N
823833
*/
824834
_applyLimit(): void {
825835
if (this._limit > 0 && this._limit < this.heapArray.length) {
826836
let rm = this.heapArray.length - this._limit;
827-
// It's much faster than splice
828-
while (rm) {
829-
this.heapArray.pop();
837+
while (rm > 0) {
838+
const worstIdx = this._worstIndex();
839+
// Swap with last and pop (standard heap removal for non-root)
840+
if (worstIdx === this.heapArray.length - 1) {
841+
this.heapArray.pop();
842+
} else {
843+
this.heapArray[worstIdx] = this.heapArray.pop() as T;
844+
this._sortNodeUp(worstIdx);
845+
this._sortNodeDown(worstIdx);
846+
}
830847
--rm;
831848
}
832849
}
@@ -1032,6 +1049,23 @@ export class Heap<T> implements Iterable<T> {
10321049
heap.init(list);
10331050
return heap.peek();
10341051
}
1052+
1053+
/**
1054+
* Find index of the worst element (for eviction when at limit).
1055+
* Worst is always among leaves (second half of array).
1056+
* @return {number} Index of worst element, -1 if empty
1057+
*/
1058+
_worstIndex(): number {
1059+
if (this.heapArray.length === 0) return -1;
1060+
const start = this.heapArray.length >> 1; // First leaf
1061+
let worstIdx = start;
1062+
for (let i = start + 1; i < this.heapArray.length; i++) {
1063+
if (this.compare(this.heapArray[i], this.heapArray[worstIdx]) > 0) {
1064+
worstIdx = i;
1065+
}
1066+
}
1067+
return worstIdx;
1068+
}
10351069
}
10361070

10371071
export default Heap;

src/HeapAsync.ts

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -347,11 +347,21 @@ export class HeapAsync<T> implements Iterable<Promise<T>> {
347347
* Adds an element to the heap. Aliases: `offer`.
348348
* Same as: push(element)
349349
* @param {any} element Element to be added
350-
* @return {Boolean} true
350+
* @return {Boolean} true if added, false if limit exceeded and element not good enough
351351
*/
352352
async add(element: T): Promise<boolean> {
353+
if (this._limit > 0 && this.heapArray.length >= this._limit) {
354+
const worstIdx = await this._worstIndex();
355+
if ((await this.compare(element, this.heapArray[worstIdx])) >= 0) {
356+
return false; // New element is not better than worst keeper
357+
}
358+
// Replace worst with new element
359+
this.heapArray[worstIdx] = element;
360+
await this._sortNodeUp(worstIdx);
361+
await this._sortNodeDown(worstIdx);
362+
return true;
363+
}
353364
await this._sortNodeUp(this.heapArray.push(element) - 1);
354-
this._applyLimit();
355365
return true;
356366
}
357367

@@ -367,7 +377,7 @@ export class HeapAsync<T> implements Iterable<Promise<T>> {
367377
for (const l = this.length; i < l; ++i) {
368378
await this._sortNodeUp(i);
369379
}
370-
this._applyLimit();
380+
await this._applyLimit();
371381
return true;
372382
}
373383

@@ -463,7 +473,7 @@ export class HeapAsync<T> implements Iterable<Promise<T>> {
463473
for (let i = HeapAsync.getParentIndexOf(this.length - 1); i >= 0; --i) {
464474
await this._sortNodeDown(i);
465475
}
466-
this._applyLimit();
476+
await this._applyLimit();
467477
}
468478

469479
/**
@@ -502,12 +512,24 @@ export class HeapAsync<T> implements Iterable<Promise<T>> {
502512
}
503513

504514
/**
505-
* Set length limit of the heap.
506-
* @return {Number}
515+
* Set length limit of the heap without eviction.
516+
* Note: Use setLimit() for async limit application with proper eviction.
517+
* @param {Number} _l Limit value
507518
*/
508519
set limit(_l: number) {
509520
this._limit = ~~_l;
510-
this._applyLimit();
521+
// Note: This setter cannot await _applyLimit(). Use setLimit() for async application.
522+
}
523+
524+
/**
525+
* Set length limit of the heap with async limit application.
526+
* @param {Number} _l Limit value
527+
* @return {Promise<number>} The limit value
528+
*/
529+
async setLimit(_l: number): Promise<number> {
530+
this._limit = ~~_l;
531+
await this._applyLimit();
532+
return this._limit;
511533
}
512534

513535
/**
@@ -699,14 +721,21 @@ export class HeapAsync<T> implements Iterable<Promise<T>> {
699721
}
700722

701723
/**
702-
* Limit heap size if needed
724+
* Limit heap size if needed, removing worst elements to keep best N
703725
*/
704-
_applyLimit(): void {
705-
if (this._limit && this._limit < this.heapArray.length) {
726+
async _applyLimit(): Promise<void> {
727+
if (this._limit > 0 && this._limit < this.heapArray.length) {
706728
let rm = this.heapArray.length - this._limit;
707-
// It's much faster than splice
708-
while (rm) {
709-
this.heapArray.pop();
729+
while (rm > 0) {
730+
const worstIdx = await this._worstIndex();
731+
// Swap with last and pop (standard heap removal for non-root)
732+
if (worstIdx === this.heapArray.length - 1) {
733+
this.heapArray.pop();
734+
} else {
735+
this.heapArray[worstIdx] = this.heapArray.pop() as T;
736+
await this._sortNodeUp(worstIdx);
737+
await this._sortNodeDown(worstIdx);
738+
}
710739
--rm;
711740
}
712741
}
@@ -914,6 +943,23 @@ export class HeapAsync<T> implements Iterable<Promise<T>> {
914943
await heap.init(list);
915944
return heap.peek();
916945
}
946+
947+
/**
948+
* Find index of the worst element (for eviction when at limit).
949+
* Worst is always among leaves (second half of array).
950+
* @return {number} Index of worst element, -1 if empty
951+
*/
952+
async _worstIndex(): Promise<number> {
953+
if (this.heapArray.length === 0) return -1;
954+
const start = this.heapArray.length >> 1; // First leaf
955+
let worstIdx = start;
956+
for (let i = start + 1; i < this.heapArray.length; i++) {
957+
if ((await this.compare(this.heapArray[i], this.heapArray[worstIdx])) > 0) {
958+
worstIdx = i;
959+
}
960+
}
961+
return worstIdx;
962+
}
917963
}
918964

919965
export default HeapAsync;

tests/heap-async/heap-async-private-methods.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,11 @@ describe('HeapAsync private', function () {
165165
expect(await heap._topIdxOf(top)).toEqual(mid);
166166
});
167167
});
168+
describe('#_worstIndex()', function () {
169+
it('should return -1 for an empty heap', async function () {
170+
expect(await heap._worstIndex()).toBe(-1);
171+
});
172+
});
168173
});
169174
});
170175
});

tests/heap-async/heap-async-public-methods.test.ts

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,10 +218,10 @@ describe('HeapAsync instances', function () {
218218
});
219219

220220
describe('#limit', function () {
221-
it('should limit the heap length', async function () {
221+
it('should limit the heap length using setLimit', async function () {
222222
await heap.init(values);
223223
expect(heap.length).toEqual(values.length);
224-
heap.limit = 5;
224+
await heap.setLimit(5);
225225
expect(heap.limit).toEqual(5);
226226
expect(heap.length).toEqual(5);
227227
const otherValues = values.slice(0, Math.floor(values.length / 2));
@@ -231,6 +231,63 @@ describe('HeapAsync instances', function () {
231231
});
232232
});
233233

234+
describe('#limit keeps top N best values', function () {
235+
it('should keep the N smallest values for min-heap', async function () {
236+
const minHeap = new HeapAsync(HeapAsync.minComparatorNumber);
237+
await minHeap.setLimit(3);
238+
await minHeap.push(5, 1, 8, 2, 9, 3, 7);
239+
expect(minHeap.length).toEqual(3);
240+
expect(minHeap.toArray().sort((a, b) => a - b)).toEqual([1, 2, 3]);
241+
});
242+
243+
it('should keep the N largest values for max-heap', async function () {
244+
const maxHeap = new HeapAsync(HeapAsync.maxComparatorNumber);
245+
await maxHeap.setLimit(3);
246+
await maxHeap.push(5, 1, 8, 2, 9, 3, 7);
247+
expect(maxHeap.length).toEqual(3);
248+
expect(maxHeap.toArray().sort((a, b) => b - a)).toEqual([9, 8, 7]);
249+
});
250+
251+
it('should keep the N best values when limit is set after init', async function () {
252+
const minHeap = new HeapAsync(HeapAsync.minComparatorNumber);
253+
await minHeap.init([5, 1, 8, 2, 9, 3, 7]);
254+
await minHeap.setLimit(3);
255+
expect(minHeap.length).toEqual(3);
256+
expect(minHeap.toArray().sort((a, b) => a - b)).toEqual([1, 2, 3]);
257+
});
258+
259+
it('should reject elements worse than the worst kept when at capacity', async function () {
260+
const minHeap = new HeapAsync(HeapAsync.minComparatorNumber);
261+
await minHeap.setLimit(3);
262+
await minHeap.push(1, 2, 3);
263+
expect(await minHeap.add(4)).toBe(false); // 4 is worse than 3
264+
expect(await minHeap.add(5)).toBe(false); // 5 is worse than 3
265+
expect(minHeap.length).toEqual(3);
266+
expect(minHeap.toArray().sort((a, b) => a - b)).toEqual([1, 2, 3]);
267+
});
268+
269+
it('should accept elements better than the worst kept when at capacity', async function () {
270+
const minHeap = new HeapAsync(HeapAsync.minComparatorNumber);
271+
await minHeap.setLimit(3);
272+
await minHeap.push(3, 4, 5);
273+
expect(await minHeap.add(2)).toBe(true); // 2 is better than 5
274+
expect(await minHeap.add(1)).toBe(true); // 1 is better than 4
275+
expect(minHeap.length).toEqual(3);
276+
expect(minHeap.toArray().sort((a, b) => a - b)).toEqual([1, 2, 3]);
277+
});
278+
279+
it('should maintain heap property after limit enforcement', async function () {
280+
const minHeap = new HeapAsync(HeapAsync.minComparatorNumber);
281+
await minHeap.setLimit(5);
282+
for (let i = 100; i > 0; i--) {
283+
await minHeap.push(i);
284+
}
285+
expect(minHeap.length).toEqual(5);
286+
expect(await minHeap.check()).toBeUndefined();
287+
expect(minHeap.toArray().sort((a, b) => a - b)).toEqual([1, 2, 3, 4, 5]);
288+
});
289+
});
290+
234291
describe('#peek()', function () {
235292
it('should return the top element of the heap', async function () {
236293
const minValue = top(...values);

tests/heap/heap-private-methods.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,11 @@ describe('Heap private', function () {
147147
expect(heap._topIdxOf(top)).toEqual(mid);
148148
});
149149
});
150+
describe('#_worstIndex()', function () {
151+
it('should return -1 for an empty heap', function () {
152+
expect(heap._worstIndex()).toBe(-1);
153+
});
154+
});
150155
});
151156
});
152157
});

tests/heap/heap-public-methods.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,63 @@ describe('Heap instances', function () {
421421
});
422422
});
423423

424+
describe('#limit keeps top N best values', function () {
425+
it('should keep the N smallest values for min-heap', function () {
426+
const minHeap = new Heap(Heap.minComparatorNumber);
427+
minHeap.limit = 3;
428+
minHeap.push(5, 1, 8, 2, 9, 3, 7);
429+
expect(minHeap.length).toEqual(3);
430+
expect(minHeap.toArray().sort((a, b) => a - b)).toEqual([1, 2, 3]);
431+
});
432+
433+
it('should keep the N largest values for max-heap', function () {
434+
const maxHeap = new Heap(Heap.maxComparatorNumber);
435+
maxHeap.limit = 3;
436+
maxHeap.push(5, 1, 8, 2, 9, 3, 7);
437+
expect(maxHeap.length).toEqual(3);
438+
expect(maxHeap.toArray().sort((a, b) => b - a)).toEqual([9, 8, 7]);
439+
});
440+
441+
it('should keep the N best values when limit is set after init', function () {
442+
const minHeap = new Heap(Heap.minComparatorNumber);
443+
minHeap.init([5, 1, 8, 2, 9, 3, 7]);
444+
minHeap.limit = 3;
445+
expect(minHeap.length).toEqual(3);
446+
expect(minHeap.toArray().sort((a, b) => a - b)).toEqual([1, 2, 3]);
447+
});
448+
449+
it('should reject elements worse than the worst kept when at capacity', function () {
450+
const minHeap = new Heap(Heap.minComparatorNumber);
451+
minHeap.limit = 3;
452+
minHeap.push(1, 2, 3);
453+
expect(minHeap.add(4)).toBe(false); // 4 is worse than 3 (the worst kept)
454+
expect(minHeap.add(5)).toBe(false); // 5 is worse than 3
455+
expect(minHeap.length).toEqual(3);
456+
expect(minHeap.toArray().sort((a, b) => a - b)).toEqual([1, 2, 3]);
457+
});
458+
459+
it('should accept elements better than the worst kept when at capacity', function () {
460+
const minHeap = new Heap(Heap.minComparatorNumber);
461+
minHeap.limit = 3;
462+
minHeap.push(3, 4, 5);
463+
expect(minHeap.add(2)).toBe(true); // 2 is better than 5 (the worst kept)
464+
expect(minHeap.add(1)).toBe(true); // 1 is better than 4
465+
expect(minHeap.length).toEqual(3);
466+
expect(minHeap.toArray().sort((a, b) => a - b)).toEqual([1, 2, 3]);
467+
});
468+
469+
it('should maintain heap property after limit enforcement', function () {
470+
const minHeap = new Heap(Heap.minComparatorNumber);
471+
minHeap.limit = 5;
472+
for (let i = 100; i > 0; i--) {
473+
minHeap.push(i);
474+
}
475+
expect(minHeap.length).toEqual(5);
476+
expect(minHeap.check()).toBeUndefined();
477+
expect(minHeap.toArray().sort((a, b) => a - b)).toEqual([1, 2, 3, 4, 5]);
478+
});
479+
});
480+
424481
describe('#peek()', function () {
425482
it('should return the top element of the heap', function () {
426483
const minValue = top(...values);

0 commit comments

Comments
 (0)