Spaces:
Configuration error
Configuration error
| // | |
| // list | |
| // ββββββββ | |
| // ββββββββββββββββΌβhead β | |
| // β β tailββΌβββββββββββββββ | |
| // β ββββββββ β | |
| // βΌ βΌ | |
| // item item item item | |
| // ββββββββ ββββββββ ββββββββ ββββββββ | |
| // null ββββΌβprev ββββββΌβprev ββββββΌβprev ββββββΌβprev β | |
| // β nextββΌββββΆβ nextββΌββββΆβ nextββΌββββΆβ nextββΌβββΆ null | |
| // ββββββββ€ ββββββββ€ ββββββββ€ ββββββββ€ | |
| // β data β β data β β data β β data β | |
| // ββββββββ ββββββββ ββββββββ ββββββββ | |
| // | |
| let releasedCursors = null; | |
| export class List { | |
| static createItem(data) { | |
| return { | |
| prev: null, | |
| next: null, | |
| data | |
| }; | |
| } | |
| constructor() { | |
| this.head = null; | |
| this.tail = null; | |
| this.cursor = null; | |
| } | |
| createItem(data) { | |
| return List.createItem(data); | |
| } | |
| // cursor helpers | |
| allocateCursor(prev, next) { | |
| let cursor; | |
| if (releasedCursors !== null) { | |
| cursor = releasedCursors; | |
| releasedCursors = releasedCursors.cursor; | |
| cursor.prev = prev; | |
| cursor.next = next; | |
| cursor.cursor = this.cursor; | |
| } else { | |
| cursor = { | |
| prev, | |
| next, | |
| cursor: this.cursor | |
| }; | |
| } | |
| this.cursor = cursor; | |
| return cursor; | |
| } | |
| releaseCursor() { | |
| const { cursor } = this; | |
| this.cursor = cursor.cursor; | |
| cursor.prev = null; | |
| cursor.next = null; | |
| cursor.cursor = releasedCursors; | |
| releasedCursors = cursor; | |
| } | |
| updateCursors(prevOld, prevNew, nextOld, nextNew) { | |
| let { cursor } = this; | |
| while (cursor !== null) { | |
| if (cursor.prev === prevOld) { | |
| cursor.prev = prevNew; | |
| } | |
| if (cursor.next === nextOld) { | |
| cursor.next = nextNew; | |
| } | |
| cursor = cursor.cursor; | |
| } | |
| } | |
| *[Symbol.iterator]() { | |
| for (let cursor = this.head; cursor !== null; cursor = cursor.next) { | |
| yield cursor.data; | |
| } | |
| } | |
| // getters | |
| get size() { | |
| let size = 0; | |
| for (let cursor = this.head; cursor !== null; cursor = cursor.next) { | |
| size++; | |
| } | |
| return size; | |
| } | |
| get isEmpty() { | |
| return this.head === null; | |
| } | |
| get first() { | |
| return this.head && this.head.data; | |
| } | |
| get last() { | |
| return this.tail && this.tail.data; | |
| } | |
| // convertors | |
| fromArray(array) { | |
| let cursor = null; | |
| this.head = null; | |
| for (let data of array) { | |
| const item = List.createItem(data); | |
| if (cursor !== null) { | |
| cursor.next = item; | |
| } else { | |
| this.head = item; | |
| } | |
| item.prev = cursor; | |
| cursor = item; | |
| } | |
| this.tail = cursor; | |
| return this; | |
| } | |
| toArray() { | |
| return [...this]; | |
| } | |
| toJSON() { | |
| return [...this]; | |
| } | |
| // array-like methods | |
| forEach(fn, thisArg = this) { | |
| // push cursor | |
| const cursor = this.allocateCursor(null, this.head); | |
| while (cursor.next !== null) { | |
| const item = cursor.next; | |
| cursor.next = item.next; | |
| fn.call(thisArg, item.data, item, this); | |
| } | |
| // pop cursor | |
| this.releaseCursor(); | |
| } | |
| forEachRight(fn, thisArg = this) { | |
| // push cursor | |
| const cursor = this.allocateCursor(this.tail, null); | |
| while (cursor.prev !== null) { | |
| const item = cursor.prev; | |
| cursor.prev = item.prev; | |
| fn.call(thisArg, item.data, item, this); | |
| } | |
| // pop cursor | |
| this.releaseCursor(); | |
| } | |
| reduce(fn, initialValue, thisArg = this) { | |
| // push cursor | |
| let cursor = this.allocateCursor(null, this.head); | |
| let acc = initialValue; | |
| let item; | |
| while (cursor.next !== null) { | |
| item = cursor.next; | |
| cursor.next = item.next; | |
| acc = fn.call(thisArg, acc, item.data, item, this); | |
| } | |
| // pop cursor | |
| this.releaseCursor(); | |
| return acc; | |
| } | |
| reduceRight(fn, initialValue, thisArg = this) { | |
| // push cursor | |
| let cursor = this.allocateCursor(this.tail, null); | |
| let acc = initialValue; | |
| let item; | |
| while (cursor.prev !== null) { | |
| item = cursor.prev; | |
| cursor.prev = item.prev; | |
| acc = fn.call(thisArg, acc, item.data, item, this); | |
| } | |
| // pop cursor | |
| this.releaseCursor(); | |
| return acc; | |
| } | |
| some(fn, thisArg = this) { | |
| for (let cursor = this.head; cursor !== null; cursor = cursor.next) { | |
| if (fn.call(thisArg, cursor.data, cursor, this)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| map(fn, thisArg = this) { | |
| const result = new List(); | |
| for (let cursor = this.head; cursor !== null; cursor = cursor.next) { | |
| result.appendData(fn.call(thisArg, cursor.data, cursor, this)); | |
| } | |
| return result; | |
| } | |
| filter(fn, thisArg = this) { | |
| const result = new List(); | |
| for (let cursor = this.head; cursor !== null; cursor = cursor.next) { | |
| if (fn.call(thisArg, cursor.data, cursor, this)) { | |
| result.appendData(cursor.data); | |
| } | |
| } | |
| return result; | |
| } | |
| nextUntil(start, fn, thisArg = this) { | |
| if (start === null) { | |
| return; | |
| } | |
| // push cursor | |
| const cursor = this.allocateCursor(null, start); | |
| while (cursor.next !== null) { | |
| const item = cursor.next; | |
| cursor.next = item.next; | |
| if (fn.call(thisArg, item.data, item, this)) { | |
| break; | |
| } | |
| } | |
| // pop cursor | |
| this.releaseCursor(); | |
| } | |
| prevUntil(start, fn, thisArg = this) { | |
| if (start === null) { | |
| return; | |
| } | |
| // push cursor | |
| const cursor = this.allocateCursor(start, null); | |
| while (cursor.prev !== null) { | |
| const item = cursor.prev; | |
| cursor.prev = item.prev; | |
| if (fn.call(thisArg, item.data, item, this)) { | |
| break; | |
| } | |
| } | |
| // pop cursor | |
| this.releaseCursor(); | |
| } | |
| // mutation | |
| clear() { | |
| this.head = null; | |
| this.tail = null; | |
| } | |
| copy() { | |
| const result = new List(); | |
| for (let data of this) { | |
| result.appendData(data); | |
| } | |
| return result; | |
| } | |
| prepend(item) { | |
| // head | |
| // ^ | |
| // item | |
| this.updateCursors(null, item, this.head, item); | |
| // insert to the beginning of the list | |
| if (this.head !== null) { | |
| // new item <- first item | |
| this.head.prev = item; | |
| // new item -> first item | |
| item.next = this.head; | |
| } else { | |
| // if list has no head, then it also has no tail | |
| // in this case tail points to the new item | |
| this.tail = item; | |
| } | |
| // head always points to new item | |
| this.head = item; | |
| return this; | |
| } | |
| prependData(data) { | |
| return this.prepend(List.createItem(data)); | |
| } | |
| append(item) { | |
| return this.insert(item); | |
| } | |
| appendData(data) { | |
| return this.insert(List.createItem(data)); | |
| } | |
| insert(item, before = null) { | |
| if (before !== null) { | |
| // prev before | |
| // ^ | |
| // item | |
| this.updateCursors(before.prev, item, before, item); | |
| if (before.prev === null) { | |
| // insert to the beginning of list | |
| if (this.head !== before) { | |
| throw new Error('before doesn\'t belong to list'); | |
| } | |
| // since head points to before therefore list doesn't empty | |
| // no need to check tail | |
| this.head = item; | |
| before.prev = item; | |
| item.next = before; | |
| this.updateCursors(null, item); | |
| } else { | |
| // insert between two items | |
| before.prev.next = item; | |
| item.prev = before.prev; | |
| before.prev = item; | |
| item.next = before; | |
| } | |
| } else { | |
| // tail | |
| // ^ | |
| // item | |
| this.updateCursors(this.tail, item, null, item); | |
| // insert to the ending of the list | |
| if (this.tail !== null) { | |
| // last item -> new item | |
| this.tail.next = item; | |
| // last item <- new item | |
| item.prev = this.tail; | |
| } else { | |
| // if list has no tail, then it also has no head | |
| // in this case head points to new item | |
| this.head = item; | |
| } | |
| // tail always points to new item | |
| this.tail = item; | |
| } | |
| return this; | |
| } | |
| insertData(data, before) { | |
| return this.insert(List.createItem(data), before); | |
| } | |
| remove(item) { | |
| // item | |
| // ^ | |
| // prev next | |
| this.updateCursors(item, item.prev, item, item.next); | |
| if (item.prev !== null) { | |
| item.prev.next = item.next; | |
| } else { | |
| if (this.head !== item) { | |
| throw new Error('item doesn\'t belong to list'); | |
| } | |
| this.head = item.next; | |
| } | |
| if (item.next !== null) { | |
| item.next.prev = item.prev; | |
| } else { | |
| if (this.tail !== item) { | |
| throw new Error('item doesn\'t belong to list'); | |
| } | |
| this.tail = item.prev; | |
| } | |
| item.prev = null; | |
| item.next = null; | |
| return item; | |
| } | |
| push(data) { | |
| this.insert(List.createItem(data)); | |
| } | |
| pop() { | |
| return this.tail !== null ? this.remove(this.tail) : null; | |
| } | |
| unshift(data) { | |
| this.prepend(List.createItem(data)); | |
| } | |
| shift() { | |
| return this.head !== null ? this.remove(this.head) : null; | |
| } | |
| prependList(list) { | |
| return this.insertList(list, this.head); | |
| } | |
| appendList(list) { | |
| return this.insertList(list); | |
| } | |
| insertList(list, before) { | |
| // ignore empty lists | |
| if (list.head === null) { | |
| return this; | |
| } | |
| if (before !== undefined && before !== null) { | |
| this.updateCursors(before.prev, list.tail, before, list.head); | |
| // insert in the middle of dist list | |
| if (before.prev !== null) { | |
| // before.prev <-> list.head | |
| before.prev.next = list.head; | |
| list.head.prev = before.prev; | |
| } else { | |
| this.head = list.head; | |
| } | |
| before.prev = list.tail; | |
| list.tail.next = before; | |
| } else { | |
| this.updateCursors(this.tail, list.tail, null, list.head); | |
| // insert to end of the list | |
| if (this.tail !== null) { | |
| // if destination list has a tail, then it also has a head, | |
| // but head doesn't change | |
| // dest tail -> source head | |
| this.tail.next = list.head; | |
| // dest tail <- source head | |
| list.head.prev = this.tail; | |
| } else { | |
| // if list has no a tail, then it also has no a head | |
| // in this case points head to new item | |
| this.head = list.head; | |
| } | |
| // tail always start point to new item | |
| this.tail = list.tail; | |
| } | |
| list.head = null; | |
| list.tail = null; | |
| return this; | |
| } | |
| replace(oldItem, newItemOrList) { | |
| if ('head' in newItemOrList) { | |
| this.insertList(newItemOrList, oldItem); | |
| } else { | |
| this.insert(newItemOrList, oldItem); | |
| } | |
| this.remove(oldItem); | |
| } | |
| } | |