Skip to main content
revise per radarbob
Source Link
Justin Chang
  • 2.4k
  • 2
  • 13
function bucket() {
    let head = null; // [key, value, next, prev]
  
    // returns the first node with a key or null
    function find(key) {
        let node = head;
        while (node != null) {
            if (node.key == key) 
                return node;
            node = node.next;
        }
        
        return null;
    }
  
    // updates or inserts the key / value,
    // returning whether insertion was needed
    function upsert(key, value) {
        let node = find(key);
  
        // not found? insert at front
        if (node == null) {
            const new_node = { 
                key: key, 
                value: value, 
                next: head, 
                prev: null 
            };

            if (head != null) 
                head.prev = new_node;
            head = new_node;
            return true;
        }
  
        node.value = value;
        return false;
    }
  
    // removes all nodes with the given key,
    // returning whether erase was needed
    function remove(key) {
        let node = find(key);
    
        // not found? do nothing
        if (node == null) 
            return false;
    
        if (node.prev == null) 
            head = head.next;
        else 
            node.prev.next = node.next;
    
        if (node.next != null) 
            node.next.prev = node.prev;
        return true;
    }
  
    // returns the value at key or other if not found
    function access(key, other) {
        let node = find(key);
    
        if (node == null) 
            return other;
    
        return node.value;
    }
  
    // printsconverts into string form
    function toString() {
        if (head == null) 
            return '{}';

        let result = '{\n';
        let node = head;
        while (node != null) {
            result += `  ${node.key}: ${node.value}\n`;
            node = node.next;
        }

        return result + '}';
    }
  
    return {
        upsert,
        remove,
        access,
        toString
    };
}
function doubly_linked_list() {
    let head = null; // [data, next, prev]

    // gets the head
    function get_head() {
        return head;
    }
  
    // front-inserts a node with data
    function insert(data) {
        const new_node = { 
            data: data, 
            next: head, 
            prev: null 
        };

        if (head != null) 
            head.prev = new_node;
        head = new_node;
    }
  
    // erases a node (assumed to be valid)
    function erase(node) {
        if (node.prev == null) 
            head = head.next;
        else 
            node.prev.next = node.next;
    
        if (node.next != null) 
            node.next.prev = node.prev;
    }
  
    return {
        get_head,
        insert,
        erase
    };
}

function bucket() {
    const data_list = doubly_linked_list();
  
    // returns the first node with a key or null
    function find(key) {
        let node = data_list.get_head();
        while (node != null) {
            if (node.data.key == key) 
                return node;
            node = node.next;
        }
        
        return null;
    }
  
    // updates or inserts the key / value,
    // returning whether insertion was needed
    function upsert(key, value) {
        let node = find(key);
  
        // not found? insert at front
        if (node == null) {
            data_list.insert({
                key: key, 
                value: value
            });
            return true;
        }
  
        node.data.value = value;
        return false;
    }
  
    // removes all nodes with the given key,
    // returning whether erase was needed
    function remove(key) {
        let node = find(key);
    
        // not found? do nothing
        if (node == null) 
            return false;
    
        data_list.erase(node);
        return true;
    }
  
    // returns the value at key or other if not found
    function access(key, other) {
        let node = find(key);
    
        if (node == null) 
            return other;
    
        return node.data.value;
    }
  
    // printsconverts into string form
    function toString() {
        const head = data_list.get_head();
        if (head == null) 
            return '{}';

        let result = '{\n';
        let node = head;
        while (node != null) {
            result += `  ${node.data.key}: ${node.data.value}\n`;
            node = node.next;
        }

        return result + '}';
    }
  
    return {
        upsert,
        remove,
        access,
        toString
    };
}
function bucket() {
    let head = null; // [key, value, next, prev]
  
    // returns the first node with a key or null
    function find(key) {
        let node = head;
        while (node != null) {
            if (node.key == key) 
                return node;
            node = node.next;
        }
        
        return null;
    }
  
    // updates or inserts the key / value,
    // returning whether insertion was needed
    function upsert(key, value) {
        let node = find(key);
  
        // not found? insert at front
        if (node == null) {
            const new_node = { 
                key: key, 
                value: value, 
                next: head, 
                prev: null 
            };

            if (head != null) 
                head.prev = new_node;
            head = new_node;
            return true;
        }
  
        node.value = value;
        return false;
    }
  
    // removes all nodes with the given key,
    // returning whether erase was needed
    function remove(key) {
        let node = find(key);
    
        // not found? do nothing
        if (node == null) 
            return false;
    
        if (node.prev == null) 
            head = head.next;
        else 
            node.prev.next = node.next;
    
        if (node.next != null) 
            node.next.prev = node.prev;
        return true;
    }
  
    // returns the value at key or other if not found
    function access(key, other) {
        let node = find(key);
    
        if (node == null) 
            return other;
    
        return node.value;
    }
  
    // prints in string form
    function toString() {
        if (head == null) 
            return '{}';

        let result = '{\n';
        let node = head;
        while (node != null) {
            result += `  ${node.key}: ${node.value}\n`;
            node = node.next;
        }

        return result + '}';
    }
  
    return {
        upsert,
        remove,
        access,
        toString
    };
}
function doubly_linked_list() {
    let head = null; // [data, next, prev]

    // gets the head
    function get_head() {
        return head;
    }
  
    // front-inserts a node with data
    function insert(data) {
        const new_node = { 
            data: data, 
            next: head, 
            prev: null 
        };

        if (head != null) 
            head.prev = new_node;
        head = new_node;
    }
  
    // erases a node (assumed to be valid)
    function erase(node) {
        if (node.prev == null) 
            head = head.next;
        else 
            node.prev.next = node.next;
    
        if (node.next != null) 
            node.next.prev = node.prev;
    }
  
    return {
        get_head,
        insert,
        erase
    };
}

function bucket() {
    const data_list = doubly_linked_list();
  
    // returns the first node with a key or null
    function find(key) {
        let node = data_list.get_head();
        while (node != null) {
            if (node.data.key == key) 
                return node;
            node = node.next;
        }
        
        return null;
    }
  
    // updates or inserts the key / value,
    // returning whether insertion was needed
    function upsert(key, value) {
        let node = find(key);
  
        // not found? insert at front
        if (node == null) {
            data_list.insert({
                key: key, 
                value: value
            });
            return true;
        }
  
        node.data.value = value;
        return false;
    }
  
    // removes all nodes with the given key,
    // returning whether erase was needed
    function remove(key) {
        let node = find(key);
    
        // not found? do nothing
        if (node == null) 
            return false;
    
        data_list.erase(node);
        return true;
    }
  
    // returns the value at key or other if not found
    function access(key, other) {
        let node = find(key);
    
        if (node == null) 
            return other;
    
        return node.data.value;
    }
  
    // prints in string form
    function toString() {
        const head = data_list.get_head();
        if (head == null) 
            return '{}';

        let result = '{\n';
        let node = head;
        while (node != null) {
            result += `  ${node.data.key}: ${node.data.value}\n`;
            node = node.next;
        }

        return result + '}';
    }
  
    return {
        upsert,
        remove,
        access,
        toString
    };
}
function bucket() {
    let head = null; // [key, value, next, prev]
  
    // returns the first node with a key or null
    function find(key) {
        let node = head;
        while (node != null) {
            if (node.key == key) 
                return node;
            node = node.next;
        }
        
        return null;
    }
  
    // updates or inserts the key / value,
    // returning whether insertion was needed
    function upsert(key, value) {
        let node = find(key);
  
        // not found? insert at front
        if (node == null) {
            const new_node = { 
                key: key, 
                value: value, 
                next: head, 
                prev: null 
            };

            if (head != null) 
                head.prev = new_node;
            head = new_node;
            return true;
        }
  
        node.value = value;
        return false;
    }
  
    // removes all nodes with the given key,
    // returning whether erase was needed
    function remove(key) {
        let node = find(key);
    
        // not found? do nothing
        if (node == null) 
            return false;
    
        if (node.prev == null) 
            head = head.next;
        else 
            node.prev.next = node.next;
    
        if (node.next != null) 
            node.next.prev = node.prev;
        return true;
    }
  
    // returns the value at key or other if not found
    function access(key, other) {
        let node = find(key);
    
        if (node == null) 
            return other;
    
        return node.value;
    }
  
    // converts to string form
    function toString() {
        if (head == null) 
            return '{}';

        let result = '{\n';
        let node = head;
        while (node != null) {
            result += `  ${node.key}: ${node.value}\n`;
            node = node.next;
        }

        return result + '}';
    }
  
    return {
        upsert,
        remove,
        access,
        toString
    };
}
function doubly_linked_list() {
    let head = null; // [data, next, prev]

    // gets the head
    function get_head() {
        return head;
    }
  
    // front-inserts a node with data
    function insert(data) {
        const new_node = { 
            data: data, 
            next: head, 
            prev: null 
        };

        if (head != null) 
            head.prev = new_node;
        head = new_node;
    }
  
    // erases a node (assumed to be valid)
    function erase(node) {
        if (node.prev == null) 
            head = head.next;
        else 
            node.prev.next = node.next;
    
        if (node.next != null) 
            node.next.prev = node.prev;
    }
  
    return {
        get_head,
        insert,
        erase
    };
}

function bucket() {
    const data_list = doubly_linked_list();
  
    // returns the first node with a key or null
    function find(key) {
        let node = data_list.get_head();
        while (node != null) {
            if (node.data.key == key) 
                return node;
            node = node.next;
        }
        
        return null;
    }
  
    // updates or inserts the key / value,
    // returning whether insertion was needed
    function upsert(key, value) {
        let node = find(key);
  
        // not found? insert at front
        if (node == null) {
            data_list.insert({
                key: key, 
                value: value
            });
            return true;
        }
  
        node.data.value = value;
        return false;
    }
  
    // removes all nodes with the given key,
    // returning whether erase was needed
    function remove(key) {
        let node = find(key);
    
        // not found? do nothing
        if (node == null) 
            return false;
    
        data_list.erase(node);
        return true;
    }
  
    // returns the value at key or other if not found
    function access(key, other) {
        let node = find(key);
    
        if (node == null) 
            return other;
    
        return node.data.value;
    }
  
    // converts to string form
    function toString() {
        const head = data_list.get_head();
        if (head == null) 
            return '{}';

        let result = '{\n';
        let node = head;
        while (node != null) {
            result += `  ${node.data.key}: ${node.data.value}\n`;
            node = node.next;
        }

        return result + '}';
    }
  
    return {
        upsert,
        remove,
        access,
        toString
    };
}
better naming of erase
Source Link
Justin Chang
  • 2.4k
  • 2
  • 13
function bucket() {
    let head = null; // [key, value, next, prev]
  
    // returns the first node with a key or null
    function find(key) {
        let node = head;
        while (node != null) {
            if (node.key == key) 
                return node;
            node = node.next;
        }
        
        return null;
    }
  
    // updates or inserts the key / value,
    // returning whether insertion was needed
    function upsert(key, value) {
        let node = find(key);
  
        // not found? insert at front
        if (node == null) {
            const new_node = { 
                key: key, 
                value: value, 
                next: head, 
                prev: null 
            };

            if (head != null) 
                head.prev = new_node;
            head = new_node;
            return true;
        }
  
        node.value = value;
        return false;
    }
  
    // deletesremoves all nodes with athe given key,
    // returning whether deletionerase was needed
    function remove(key) {
        let node = find(key);
    
        // not found? do nothing
        if (node == null) 
            return false;
    
        if (node.prev == null) 
            head = head.next;
        else 
            node.prev.next = node.next;
    
        if (node.next != null) 
            node.next.prev = node.prev;
        return true;
    }
  
    // returns the value at key or other if not found
    function access(key, other) {
        let node = find(key);
    
        if (node == null) 
            return other;
    
        return node.value;
    }
  
    // prints in string form
    function toString() {
        if (head == null) 
            return '{}';

        let result = '{\n';
        let node = head;
        while (node != null) {
            result += `  ${node.key}: ${node.value}\n`;
            node = node.next;
        }

        return result + '}';
    }
  
    return {
        upsert,
        remove,
        access,
        toString
    };
}
function doubly_linked_list() {
    let head = null; // [data, next, prev]

    // gets the head
    function get_head() {
        return head;
    }
  
    // front-inserts a node with data
    function insert(data) {
        const new_node = { 
            data: data, 
            next: head, 
            prev: null 
        };

        if (head != null) 
            head.prev = new_node;
        head = new_node;
    }
  
    // erases a node (assumed to be valid)
    function erase(node) {
        // update the prev node
        if (node.prev == null) 
            head = head.next;
        else 
            node.prev.next = node.next;
    
        // update the next node
        if (node.next != null) 
            node.next.prev = node.prev;
    }
  
    return {
        get_head,
        insert,
        erase
    };
}

function bucket() {
    const data_list = doubly_linked_list();
  
    // returns the first node with a key or null
    function find(key) {
        let node = data_list.get_head();
        while (node != null) {
            if (node.data.key == key) 
                return node;
            node = node.next;
        }
        
        return null;
    }
  
    // updates or inserts the value for thekey given/ keyvalue,
    // returning whether insertion was needed
    function upsert(key, value) {
        let node = find(key);
  
        // not found? insert at front
        if (node == null) {
            data_list.insert({
                key: key, 
                value: value
            });
            return true;
        }
  
        node.data.value = value;
        return false;
    }
  
    // deletesremoves all nodes with the given key,
    // returning whether deletionerase was needed
    function remove(key) {
        let node = find(key);
    
        // not found? do nothing
        if (node == null) 
            return false;
    
        data_list.erase(node);
        return true;
    }
  
    // returns the value at key or other if not found
    function access(key, other) {
        let node = find(key);
    
        if (node == null) 
            return other;
    
        return node.data.value;
    }
  
    // prints in string form
    function toString() {
        const head = data_list.get_head();
        if (head == null) 
            return '{}';

        let result = '{\n';
        let node = head;
        while (node != null) {
            result += `  ${node.data.key}: ${node.data.value}\n`;
            node = node.next;
        }

        return result + '}';
    }
  
    return {
        upsert,
        remove,
        access,
        toString
    };
}
function bucket() {
    let head = null; // [key, value, next, prev]
  
    // returns the first node with a key or null
    function find(key) {
        let node = head;
        while (node != null) {
            if (node.key == key) 
                return node;
            node = node.next;
        }
        
        return null;
    }
  
    // updates or inserts the key / value,
    // returning whether insertion was needed
    function upsert(key, value) {
        let node = find(key);
  
        // not found? insert at front
        if (node == null) {
            const new_node = { 
                key: key, 
                value: value, 
                next: head, 
                prev: null 
            };

            if (head != null) 
                head.prev = new_node;
            head = new_node;
            return true;
        }
  
        node.value = value;
        return false;
    }
  
    // deletes all nodes with a key,
    // returning whether deletion was needed
    function remove(key) {
        let node = find(key);
    
        // not found? do nothing
        if (node == null) 
            return false;
    
        if (node.prev == null) 
            head = head.next;
        else 
            node.prev.next = node.next;
    
        if (node.next != null) 
            node.next.prev = node.prev;
        return true;
    }
  
    // returns the value at key or other if not found
    function access(key, other) {
        let node = find(key);
    
        if (node == null) 
            return other;
    
        return node.value;
    }
  
    // prints in string form
    function toString() {
        if (head == null) 
            return '{}';

        let result = '{\n';
        let node = head;
        while (node != null) {
            result += `  ${node.key}: ${node.value}\n`;
            node = node.next;
        }

        return result + '}';
    }
  
    return {
        upsert,
        remove,
        access,
        toString
    };
}
function doubly_linked_list() {
    let head = null; // [data, next, prev]

    // gets the head
    function get_head() {
        return head;
    }
  
    // front-inserts a node with data
    function insert(data) {
        const new_node = { 
            data: data, 
            next: head, 
            prev: null 
        };

        if (head != null) 
            head.prev = new_node;
        head = new_node;
    }
  
    // erases a node (assumed to be valid)
    function erase(node) {
        // update the prev node
        if (node.prev == null) 
            head = head.next;
        else 
            node.prev.next = node.next;
    
        // update the next node
        if (node.next != null) 
            node.next.prev = node.prev;
    }
  
    return {
        get_head,
        insert,
        erase
    };
}

function bucket() {
    const data_list = doubly_linked_list();
  
    // returns the first node with a key or null
    function find(key) {
        let node = data_list.get_head();
        while (node != null) {
            if (node.data.key == key) 
                return node;
            node = node.next;
        }
        
        return null;
    }
  
    // updates or inserts the value for the given key,
    // returning whether insertion was needed
    function upsert(key, value) {
        let node = find(key);
  
        // not found? insert at front
        if (node == null) {
            data_list.insert({
                key: key, 
                value: value
            });
            return true;
        }
  
        node.data.value = value;
        return false;
    }
  
    // deletes all nodes with the given key,
    // returning whether deletion was needed
    function remove(key) {
        let node = find(key);
    
        // not found? do nothing
        if (node == null) 
            return false;
    
        data_list.erase(node);
        return true;
    }
  
    // returns the value at key or other if not found
    function access(key, other) {
        let node = find(key);
    
        if (node == null) 
            return other;
    
        return node.data.value;
    }
  
    // prints in string form
    function toString() {
        const head = data_list.get_head();
        if (head == null) 
            return '{}';

        let result = '{\n';
        let node = head;
        while (node != null) {
            result += `  ${node.data.key}: ${node.data.value}\n`;
            node = node.next;
        }

        return result + '}';
    }
  
    return {
        upsert,
        remove,
        access,
        toString
    };
}
function bucket() {
    let head = null; // [key, value, next, prev]
  
    // returns the first node with a key or null
    function find(key) {
        let node = head;
        while (node != null) {
            if (node.key == key) 
                return node;
            node = node.next;
        }
        
        return null;
    }
  
    // updates or inserts the key / value,
    // returning whether insertion was needed
    function upsert(key, value) {
        let node = find(key);
  
        // not found? insert at front
        if (node == null) {
            const new_node = { 
                key: key, 
                value: value, 
                next: head, 
                prev: null 
            };

            if (head != null) 
                head.prev = new_node;
            head = new_node;
            return true;
        }
  
        node.value = value;
        return false;
    }
  
    // removes all nodes with the given key,
    // returning whether erase was needed
    function remove(key) {
        let node = find(key);
    
        // not found? do nothing
        if (node == null) 
            return false;
    
        if (node.prev == null) 
            head = head.next;
        else 
            node.prev.next = node.next;
    
        if (node.next != null) 
            node.next.prev = node.prev;
        return true;
    }
  
    // returns the value at key or other if not found
    function access(key, other) {
        let node = find(key);
    
        if (node == null) 
            return other;
    
        return node.value;
    }
  
    // prints in string form
    function toString() {
        if (head == null) 
            return '{}';

        let result = '{\n';
        let node = head;
        while (node != null) {
            result += `  ${node.key}: ${node.value}\n`;
            node = node.next;
        }

        return result + '}';
    }
  
    return {
        upsert,
        remove,
        access,
        toString
    };
}
function doubly_linked_list() {
    let head = null; // [data, next, prev]

    // gets the head
    function get_head() {
        return head;
    }
  
    // front-inserts a node with data
    function insert(data) {
        const new_node = { 
            data: data, 
            next: head, 
            prev: null 
        };

        if (head != null) 
            head.prev = new_node;
        head = new_node;
    }
  
    // erases a node (assumed to be valid)
    function erase(node) {
        if (node.prev == null) 
            head = head.next;
        else 
            node.prev.next = node.next;
    
        if (node.next != null) 
            node.next.prev = node.prev;
    }
  
    return {
        get_head,
        insert,
        erase
    };
}

function bucket() {
    const data_list = doubly_linked_list();
  
    // returns the first node with a key or null
    function find(key) {
        let node = data_list.get_head();
        while (node != null) {
            if (node.data.key == key) 
                return node;
            node = node.next;
        }
        
        return null;
    }
  
    // updates or inserts the key / value,
    // returning whether insertion was needed
    function upsert(key, value) {
        let node = find(key);
  
        // not found? insert at front
        if (node == null) {
            data_list.insert({
                key: key, 
                value: value
            });
            return true;
        }
  
        node.data.value = value;
        return false;
    }
  
    // removes all nodes with the given key,
    // returning whether erase was needed
    function remove(key) {
        let node = find(key);
    
        // not found? do nothing
        if (node == null) 
            return false;
    
        data_list.erase(node);
        return true;
    }
  
    // returns the value at key or other if not found
    function access(key, other) {
        let node = find(key);
    
        if (node == null) 
            return other;
    
        return node.data.value;
    }
  
    // prints in string form
    function toString() {
        const head = data_list.get_head();
        if (head == null) 
            return '{}';

        let result = '{\n';
        let node = head;
        while (node != null) {
            result += `  ${node.data.key}: ${node.data.value}\n`;
            node = node.next;
        }

        return result + '}';
    }
  
    return {
        upsert,
        remove,
        access,
        toString
    };
}
Source Link
Justin Chang
  • 2.4k
  • 2
  • 13

Great job on making a reproducible example.

My first concern is that linkedList does not appear to be a clean abstraction. Let's consider the C++ class std::unordered_map, which has a similar approach to The Odin Project:

  1. A bucket is a (doubly) linked list of key-value pairs.
  2. A hash table stores an array of buckets (resized on rehashing), the number of buckets, and the total number of elements (inserted key-value pairs).

It makes use of three abstractions:

  1. The (doubly) linked list needs traversal, data insertion (insert), and node deletion (erase).
  2. The bucket needs insert to achieve update-or-insertion (upsert) by key (returning whether the linked list grew due to not finding a key), delete to achieve deletion-or-nothing (remove) by key (returning whether the linked list shrank due to finding a key), and traversal to achieve value access (access) by key (returning the value). For all of the above, it needs to find a node by key.
  3. The hash table needs to hash keys to corresponding buckets for access and keep a running count of element sizes by observing the return values of upsert and remove. If the number of elements divided by the number of buckets exceeds the load factor, it needs to rehash appropriately.

The code above doesn't quite work with these layers (although all abstractions leak somewhat). The data structure resembles a linked list, but find should be for buckets and size should be counted by hash tables. I don't think tail is necessary - if anything, insertion at the front rather than the back would take better advantage of temporal locality (more recently inserted values are more likely to be accessed).

The repeated use of traverse (recursive) is confusing to me and I think it can be made much simpler. Here is my best attempt at the bucket level:

function bucket() {
    let head = null; // [key, value, next, prev]
  
    // returns the first node with a key or null
    function find(key) {
        let node = head;
        while (node != null) {
            if (node.key == key) 
                return node;
            node = node.next;
        }
        
        return null;
    }
  
    // updates or inserts the key / value,
    // returning whether insertion was needed
    function upsert(key, value) {
        let node = find(key);
  
        // not found? insert at front
        if (node == null) {
            const new_node = { 
                key: key, 
                value: value, 
                next: head, 
                prev: null 
            };

            if (head != null) 
                head.prev = new_node;
            head = new_node;
            return true;
        }
  
        node.value = value;
        return false;
    }
  
    // deletes all nodes with a key,
    // returning whether deletion was needed
    function remove(key) {
        let node = find(key);
    
        // not found? do nothing
        if (node == null) 
            return false;
    
        if (node.prev == null) 
            head = head.next;
        else 
            node.prev.next = node.next;
    
        if (node.next != null) 
            node.next.prev = node.prev;
        return true;
    }
  
    // returns the value at key or other if not found
    function access(key, other) {
        let node = find(key);
    
        if (node == null) 
            return other;
    
        return node.value;
    }
  
    // prints in string form
    function toString() {
        if (head == null) 
            return '{}';

        let result = '{\n';
        let node = head;
        while (node != null) {
            result += `  ${node.key}: ${node.value}\n`;
            node = node.next;
        }

        return result + '}';
    }
  
    return {
        upsert,
        remove,
        access,
        toString
    };
}

And here are some tests:

const test_list = bucket();

let insert = test_list.upsert(2, 'x');
console.log('test_list[2] = "x"', insert);
console.log(test_list.toString());

insert = test_list.upsert(5, 140);
console.log('test_list[5] = 140', insert);
console.log(test_list.toString());

insert = test_list.upsert(4, 'hi!');
console.log('test_list[4] = "hi"', insert);
console.log(test_list.toString());

insert = test_list.upsert(4, 'hello!');
console.log('test_list[4] = "hello!"', insert);
console.log(test_list.toString());

insert = test_list.upsert(5, test_list.access(5, 0) + 1);
console.log('test_list[5] += 1', insert);
console.log(test_list.toString());

insert = test_list.upsert(16, test_list.access(16, 0) + 1);
console.log('test_list[16] += 1', insert);
console.log(test_list.toString());

insert = test_list.upsert(80, test_list.access(80, "") + "a");
console.log('test_list[80] += "a"', insert);
console.log(test_list.toString());

let shrink = test_list.remove("abc");
console.log('test_list.remove("abc")', shrink);
console.log(test_list.toString());

shrink = test_list.remove(80);
console.log('test_list.remove(80)', shrink);
console.log(test_list.toString());

shrink = test_list.remove(4);
console.log('test_list.remove(4)', shrink);
console.log(test_list.toString());

shrink = test_list.remove(16);
console.log('test_list.remove(16)', shrink);
console.log(test_list.toString());

shrink = test_list.remove(2);
console.log('test_list.remove(2)', shrink);
console.log(test_list.toString());

shrink = test_list.remove(5);
console.log('test_list.remove(5)', shrink);
console.log(test_list.toString());

shrink = test_list.remove(5);
console.log('test_list.remove(5)', shrink);
console.log(test_list.toString());

And here is a version separating the linked list / bucket functionality:

function doubly_linked_list() {
    let head = null; // [data, next, prev]

    // gets the head
    function get_head() {
        return head;
    }
  
    // front-inserts a node with data
    function insert(data) {
        const new_node = { 
            data: data, 
            next: head, 
            prev: null 
        };

        if (head != null) 
            head.prev = new_node;
        head = new_node;
    }
  
    // erases a node (assumed to be valid)
    function erase(node) {
        // update the prev node
        if (node.prev == null) 
            head = head.next;
        else 
            node.prev.next = node.next;
    
        // update the next node
        if (node.next != null) 
            node.next.prev = node.prev;
    }
  
    return {
        get_head,
        insert,
        erase
    };
}

function bucket() {
    const data_list = doubly_linked_list();
  
    // returns the first node with a key or null
    function find(key) {
        let node = data_list.get_head();
        while (node != null) {
            if (node.data.key == key) 
                return node;
            node = node.next;
        }
        
        return null;
    }
  
    // updates or inserts the value for the given key,
    // returning whether insertion was needed
    function upsert(key, value) {
        let node = find(key);
  
        // not found? insert at front
        if (node == null) {
            data_list.insert({
                key: key, 
                value: value
            });
            return true;
        }
  
        node.data.value = value;
        return false;
    }
  
    // deletes all nodes with the given key,
    // returning whether deletion was needed
    function remove(key) {
        let node = find(key);
    
        // not found? do nothing
        if (node == null) 
            return false;
    
        data_list.erase(node);
        return true;
    }
  
    // returns the value at key or other if not found
    function access(key, other) {
        let node = find(key);
    
        if (node == null) 
            return other;
    
        return node.data.value;
    }
  
    // prints in string form
    function toString() {
        const head = data_list.get_head();
        if (head == null) 
            return '{}';

        let result = '{\n';
        let node = head;
        while (node != null) {
            result += `  ${node.data.key}: ${node.data.value}\n`;
            node = node.next;
        }

        return result + '}';
    }
  
    return {
        upsert,
        remove,
        access,
        toString
    };
}

Hope this helps!