src/linked_list.c

Sun, 23 Nov 2025 13:15:19 +0100

author
Mike Becker <universe@uap-core.de>
date
Sun, 23 Nov 2025 13:15:19 +0100
changeset 1508
dfc0ddd9571e
parent 1504
467a77a85f43
permissions
-rw-r--r--

optimize sorted insertion by using the infimum instead of the supremum

The reason is that the supremum returns the equal element with the smallest index, and we want the largest.
Therefore, we use the infimum, which already gives us the largest index when there are equal elements, and increase the index by one. The infimum is also guaranteed to exist in that case.

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   1. Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *
 *   2. Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include "cx/linked_list.h"
#include "cx/compare.h"
#include <string.h>
#include <assert.h>

// LOW LEVEL LINKED LIST FUNCTIONS

#define CX_LL_PTR(cur, off) (*(void**)(((char*)(cur))+(off)))
#define ll_prev(node) CX_LL_PTR(node, loc_prev)
#define ll_next(node) CX_LL_PTR(node, loc_next)
#define ll_advance(node) CX_LL_PTR(node, loc_advance)
#define ll_data(node) (((char*)(node))+loc_data)

void *cx_linked_list_at(
        const void *start,
        size_t start_index,
        ptrdiff_t loc_advance,
        size_t index
) {
    assert(start != NULL);
    assert(loc_advance >= 0);
    size_t i = start_index;
    const void *cur = start;
    while (i != index && cur != NULL) {
        cur = ll_advance(cur);
        i < index ? i++ : i--;
    }
    return (void *) cur;
}

void *cx_linked_list_find(
        const void *start,
        ptrdiff_t loc_advance,
        ptrdiff_t loc_data,
        cx_compare_func cmp_func,
        const void *elem,
        size_t *found_index
) {
    assert(start != NULL);
    assert(loc_advance >= 0);
    assert(loc_data >= 0);
    assert(cmp_func);

    void *node = (void*) start;
    size_t index = 0;
    do {
        void *current = ll_data(node);
        if (cmp_func(current, elem) == 0) {
            if (found_index != NULL) {
                *found_index = index;
            }
            return node;
        }
        node = ll_advance(node);
        index++;
    } while (node != NULL);
    return NULL;
}

void *cx_linked_list_first(
        const void *node,
        ptrdiff_t loc_prev
) {
    return cx_linked_list_last(node, loc_prev);
}

void *cx_linked_list_last(
        const void *node,
        ptrdiff_t loc_next
) {
    assert(node != NULL);
    assert(loc_next >= 0);

    const void *cur = node;
    const void *last;
    do {
        last = cur;
    } while ((cur = ll_next(cur)) != NULL);

    return (void *) last;
}

void *cx_linked_list_prev(
        const void *begin,
        ptrdiff_t loc_next,
        const void *node
) {
    assert(begin != NULL);
    assert(node != NULL);
    assert(loc_next >= 0);
    if (begin == node) return NULL;
    const void *cur = begin;
    const void *next;
    while (1) {
        next = ll_next(cur);
        if (next == node) return (void *) cur;
        cur = next;
    }
}

void cx_linked_list_link(
        void *left,
        void *right,
        ptrdiff_t loc_prev,
        ptrdiff_t loc_next
) {
    assert(loc_next >= 0);
    ll_next(left) = right;
    if (loc_prev >= 0) {
        ll_prev(right) = left;
    }
}

void cx_linked_list_unlink(
        void *left,
        void *right,
        ptrdiff_t loc_prev,
        ptrdiff_t loc_next
) {
    assert (loc_next >= 0);
    assert(ll_next(left) == right);
    ll_next(left) = NULL;
    if (loc_prev >= 0) {
        assert(ll_prev(right) == left);
        ll_prev(right) = NULL;
    }
}

void cx_linked_list_add(
        void **begin,
        void **end,
        ptrdiff_t loc_prev,
        ptrdiff_t loc_next,
        void *new_node
) {
    void *last;
    if (end == NULL) {
        assert(begin != NULL);
        last = *begin == NULL ? NULL : cx_linked_list_last(*begin, loc_next);
    } else {
        last = *end;
    }
    cx_linked_list_insert_chain(begin, end, loc_prev, loc_next, last, new_node, new_node);
}

void cx_linked_list_prepend(
        void **begin,
        void **end,
        ptrdiff_t loc_prev,
        ptrdiff_t loc_next,
        void *new_node
) {
    cx_linked_list_insert_chain(begin, end, loc_prev, loc_next, NULL, new_node, new_node);
}

void cx_linked_list_insert(
        void **begin,
        void **end,
        ptrdiff_t loc_prev,
        ptrdiff_t loc_next,
        void *node,
        void *new_node
) {
    cx_linked_list_insert_chain(begin, end, loc_prev, loc_next, node, new_node, new_node);
}

void cx_linked_list_insert_chain(
        void **begin,
        void **end,
        ptrdiff_t loc_prev,
        ptrdiff_t loc_next,
        void *node,
        void *insert_begin,
        void *insert_end
) {
    // find the end of the chain, if not specified
    if (insert_end == NULL) {
        insert_end = cx_linked_list_last(insert_begin, loc_next);
    }

    // determine the successor
    void *successor;
    if (node == NULL) {
        assert(begin != NULL || (end != NULL && loc_prev >= 0));
        if (begin != NULL) {
            successor = *begin;
            *begin = insert_begin;
        } else {
            successor = *end == NULL ? NULL : cx_linked_list_first(*end, loc_prev);
        }
    } else {
        successor = ll_next(node);
        cx_linked_list_link(node, insert_begin, loc_prev, loc_next);
    }

    if (successor == NULL) {
        // the list ends with the new chain
        if (end != NULL) {
            *end = insert_end;
        }
    } else {
        cx_linked_list_link(insert_end, successor, loc_prev, loc_next);
    }
}

void cx_linked_list_insert_sorted(
        void **begin,
        void **end,
        ptrdiff_t loc_prev,
        ptrdiff_t loc_next,
        void *new_node,
        cx_compare_func cmp_func
) {
    assert(ll_next(new_node) == NULL);
    cx_linked_list_insert_sorted_chain(
            begin, end, loc_prev, loc_next, new_node, cmp_func);
}

static void *cx_linked_list_insert_sorted_chain_impl(
        void **begin,
        void **end,
        ptrdiff_t loc_prev,
        ptrdiff_t loc_next,
        void *insert_begin,
        cx_compare_func cmp_func,
        bool allow_duplicates
) {
    assert(begin != NULL);
    assert(loc_next >= 0);
    assert(insert_begin != NULL);

    // strategy: build completely new chains from scratch
    void *source_original = *begin;
    void *source_argument = insert_begin;
    void *new_begin = NULL;
    void *new_end = NULL;
    void *dup_begin = NULL;
    void *dup_end = NULL;

    // determine the new start
    {
        int d = source_original ==  NULL ? 1 : cmp_func(source_original, source_argument);
        if (d <= 0) {
            // the new chain starts with the original chain
            new_begin = new_end = source_original;
            source_original = ll_next(source_original);
            if (d == 0) {
                if (allow_duplicates) {
                    // duplicate allowed, append it to the chain
                    cx_linked_list_link(new_end, source_argument, loc_prev, loc_next);
                    new_end = source_argument;
                } else {
                    // duplicate is not allowed, start a duplicate chain with the argument
                    dup_begin = dup_end = source_argument;
                }
                source_argument = ll_next(source_argument);
            }
        } else {
            // input is smaller, or there is no original chain;
            // start the new chain with the source argument
            new_begin = new_end = source_argument;
            source_argument = ll_next(source_argument);
        }
    }

    // now successively compare the elements and add them to the correct chains
    while (source_original != NULL && source_argument != NULL) {
        int d = cmp_func(source_original, source_argument);
        if (d <= 0) {
            // the original is not larger, add it to the chain
            cx_linked_list_link(new_end, source_original, loc_prev, loc_next);
            new_end = source_original;
            source_original = ll_next(source_original);
            if (d == 0) {
                if (allow_duplicates) {
                    // duplicate allowed, append it to the chain
                    cx_linked_list_link(new_end, source_argument, loc_prev, loc_next);
                    new_end = source_argument;
                } else {
                    // duplicate is not allowed, append it to the duplicate chain
                    if (dup_end == NULL) {
                        dup_begin = dup_end = source_argument;
                    } else {
                        cx_linked_list_link(dup_end, source_argument, loc_prev, loc_next);
                        dup_end = source_argument;
                    }
                }
                source_argument = ll_next(source_argument);
            }
        } else {
            // the original is larger, append the source argument to the chain
            // check if we must discard the source argument as duplicate
            if (!allow_duplicates && cmp_func(new_end, source_argument) == 0) {
                if (dup_end == NULL) {
                    dup_begin = dup_end = source_argument;
                } else {
                    cx_linked_list_link(dup_end, source_argument, loc_prev, loc_next);
                    dup_end = source_argument;
                }
            } else {
                // no duplicate or duplicates allowed
                cx_linked_list_link(new_end, source_argument, loc_prev, loc_next);
                new_end = source_argument;
            }
            source_argument = ll_next(source_argument);
        }
    }

    if (source_original != NULL) {
        // something is left from the original chain, append it
        cx_linked_list_link(new_end, source_original, loc_prev, loc_next);
        new_end = cx_linked_list_last(source_original, loc_next);
    } else if (source_argument != NULL) {
        // something is left from the input chain;
        // when we allow duplicates, append it
        if (allow_duplicates) {
            cx_linked_list_link(new_end, source_argument, loc_prev, loc_next);
            new_end = cx_linked_list_last(source_argument, loc_next);
        } else {
            // otherwise we must check one-by-one
            while (source_argument != NULL) {
                if (cmp_func(new_end, source_argument) == 0) {
                    if (dup_end == NULL) {
                        dup_begin = dup_end = source_argument;
                    } else {
                        cx_linked_list_link(dup_end, source_argument, loc_prev, loc_next);
                        dup_end = source_argument;
                    }
                } else {
                    cx_linked_list_link(new_end, source_argument, loc_prev, loc_next);
                    new_end = source_argument;
                }
                source_argument = ll_next(source_argument);
            }
        }
    }

    // null the next pointers at the end of the chain
    ll_next(new_end) = NULL;
    if (dup_end != NULL) {
        ll_next(dup_end) = NULL;
    }

    // null the optional prev pointers
    if (loc_prev >= 0) {
        ll_prev(new_begin) = NULL;
        if (dup_begin != NULL) {
            ll_prev(dup_begin) = NULL;
        }
    }

    // output
    *begin = new_begin;
    if (end != NULL) {
        *end = new_end;
    }
    return dup_begin;
}

void cx_linked_list_insert_sorted_chain(
        void **begin,
        void **end,
        ptrdiff_t loc_prev,
        ptrdiff_t loc_next,
        void *insert_begin,
        cx_compare_func cmp_func
) {
    cx_linked_list_insert_sorted_chain_impl(
            begin, end, loc_prev, loc_next,
            insert_begin, cmp_func, true);
}

int cx_linked_list_insert_unique(
        void **begin,
        void **end,
        ptrdiff_t loc_prev,
        ptrdiff_t loc_next,
        void *new_node,
        cx_compare_func cmp_func
) {
    assert(ll_next(new_node) == NULL);
    return NULL != cx_linked_list_insert_unique_chain(
            begin, end, loc_prev, loc_next, new_node, cmp_func);
}

void *cx_linked_list_insert_unique_chain(
        void **begin,
        void **end,
        ptrdiff_t loc_prev,
        ptrdiff_t loc_next,
        void *insert_begin,
        cx_compare_func cmp_func
) {
    return cx_linked_list_insert_sorted_chain_impl(
            begin, end, loc_prev, loc_next,
            insert_begin, cmp_func, false);
}

size_t cx_linked_list_remove_chain(
        void **begin,
        void **end,
        ptrdiff_t loc_prev,
        ptrdiff_t loc_next,
        void *node,
        size_t num
) {
    assert(node != NULL);
    assert(loc_next >= 0);
    assert(loc_prev >= 0 || begin != NULL);

    // easy exit
    if (num == 0) return 0;

    // find adjacent nodes
    void *prev;
    if (loc_prev >= 0) {
        prev = ll_prev(node);
    } else {
        prev = cx_linked_list_prev(*begin, loc_next, node);
    }

    void *next = ll_next(node);
    size_t removed = 1;
    for (; removed < num && next != NULL ; removed++) {
        next = ll_next(next);
    }

    // update next pointer of prev node, or set begin
    if (prev == NULL) {
        if (begin != NULL) {
            *begin = next;
        }
    } else {
        ll_next(prev) = next;
    }

    // update prev pointer of next node, or set end
    if (next == NULL) {
        if (end != NULL) {
            *end = prev;
        }
    } else if (loc_prev >= 0) {
        ll_prev(next) = prev;
    }

    return removed;
}

void cx_linked_list_remove(
        void **begin,
        void **end,
        ptrdiff_t loc_prev,
        ptrdiff_t loc_next,
        void *node
) {
    cx_linked_list_remove_chain(begin, end, loc_prev, loc_next, node, 1);
}

size_t cx_linked_list_size(
        const void *node,
        ptrdiff_t loc_next
) {
    assert(loc_next >= 0);
    size_t size = 0;
    while (node != NULL) {
        node = ll_next(node);
        size++;
    }
    return size;
}

#ifndef CX_LINKED_LIST_SORT_SBO_SIZE
#define CX_LINKED_LIST_SORT_SBO_SIZE 1024
#endif

static void cx_linked_list_sort_merge(
        ptrdiff_t loc_prev,
        ptrdiff_t loc_next,
        ptrdiff_t loc_data,
        size_t length,
        void *ls,
        void *le,
        void *re,
        cx_compare_func cmp_func,
        void **begin,
        void **end
) {
    void *sbo[CX_LINKED_LIST_SORT_SBO_SIZE];
    void **sorted = length >= CX_LINKED_LIST_SORT_SBO_SIZE ?
                    cxMallocDefault(sizeof(void *) * length) : sbo;
    if (sorted == NULL) abort(); // LCOV_EXCL_LINE
    void *rc, *lc;

    lc = ls;
    rc = le;
    size_t n = 0;
    while (lc && lc != le && rc != re) {
        if (cmp_func(ll_data(lc), ll_data(rc)) <= 0) {
            sorted[n] = lc;
            lc = ll_next(lc);
        } else {
            sorted[n] = rc;
            rc = ll_next(rc);
        }
        n++;
    }
    while (lc && lc != le) {
        sorted[n] = lc;
        lc = ll_next(lc);
        n++;
    }
    while (rc && rc != re) {
        sorted[n] = rc;
        rc = ll_next(rc);
        n++;
    }

    // Update pointer
    if (loc_prev >= 0) ll_prev(sorted[0]) = NULL;
    for (size_t i = 0 ; i < length - 1; i++) {
        cx_linked_list_link(sorted[i], sorted[i + 1], loc_prev, loc_next);
    }
    ll_next(sorted[length - 1]) = NULL;

    *begin = sorted[0];
    *end = sorted[length - 1];
    if (sorted != sbo) {
        cxFreeDefault(sorted);
    }
}

void cx_linked_list_sort( // NOLINT(misc-no-recursion) - purposely recursive function
        void **begin,
        void **end,
        ptrdiff_t loc_prev,
        ptrdiff_t loc_next,
        ptrdiff_t loc_data,
        cx_compare_func cmp_func
) {
    assert(begin != NULL);
    assert(loc_next >= 0);
    assert(loc_data >= 0);
    assert(cmp_func);

    void *lc, *ls, *le, *re;

    // set start node
    ls = *begin;

    // early exit when this list is empty
    if (ls == NULL) return;

    // check how many elements are already sorted
    lc = ls;
    size_t ln = 1;
    while (ll_next(lc) != NULL && cmp_func(ll_data(ll_next(lc)), ll_data(lc)) > 0) {
        lc = ll_next(lc);
        ln++;
    }
    le = ll_next(lc);

    // if first unsorted node is NULL, the list is already completely sorted
    if (le != NULL) {
        void *rc;
        size_t rn = 1;
        rc = le;
        // skip already sorted elements
        while (ll_next(rc) != NULL && cmp_func(ll_data(ll_next(rc)), ll_data(rc)) > 0) {
            rc = ll_next(rc);
            rn++;
        }
        re = ll_next(rc);

        // {ls,...,le->prev} and {rs,...,re->prev} are sorted - merge them
        void *sorted_begin, *sorted_end;
        cx_linked_list_sort_merge(loc_prev, loc_next, loc_data,
                                  ln + rn, ls, le, re, cmp_func,
                                  &sorted_begin, &sorted_end);

        // Something left? Sort it!
        size_t remainder_length = cx_linked_list_size(re, loc_next);
        if (remainder_length > 0) {
            void *remainder = re;
            cx_linked_list_sort(&remainder, NULL, loc_prev, loc_next, loc_data, cmp_func);

            // merge sorted list with (also sorted) remainder
            cx_linked_list_sort_merge(loc_prev, loc_next, loc_data,
                                      ln + rn + remainder_length,
                                      sorted_begin, remainder, NULL, cmp_func,
                                      &sorted_begin, &sorted_end);
        }
        *begin = sorted_begin;
        if (end) *end = sorted_end;
    }
}

int cx_linked_list_compare(
        const void *begin_left,
        const void *begin_right,
        ptrdiff_t loc_advance,
        ptrdiff_t loc_data,
        cx_compare_func cmp_func
) {
    const void *left = begin_left, *right = begin_right;

    while (left != NULL && right != NULL) {
        const void *left_data = ll_data(left);
        const void *right_data = ll_data(right);
        int result = cmp_func(left_data, right_data);
        if (result != 0) return result;
        left = ll_advance(left);
        right = ll_advance(right);
    }

    if (left != NULL) { return 1; }
    else if (right != NULL) { return -1; }
    else { return 0; }
}

void cx_linked_list_reverse(
        void **begin,
        void **end,
        ptrdiff_t loc_prev,
        ptrdiff_t loc_next
) {
    assert(begin != NULL);
    assert(loc_next >= 0);

    // swap all links
    void *prev = NULL;
    void *cur = *begin;
    while (cur != NULL) {
        void *next = ll_next(cur);

        ll_next(cur) = prev;
        if (loc_prev >= 0) {
            ll_prev(cur) = next;
        }

        prev = cur;
        cur = next;
    }

    // update begin and end
    if (end != NULL) {
        *end = *begin;
    }
    *begin = prev;
}

// HIGH LEVEL LINKED LIST IMPLEMENTATION

static void *cx_ll_node_at(
        const cx_linked_list *list,
        size_t index
) {
    if (index >= list->base.collection.size) {
        return NULL;
    } else if (index > list->base.collection.size / 2) {
        return cx_linked_list_at(list->end, list->base.collection.size - 1, list->loc_prev, index);
    } else {
        return cx_linked_list_at(list->begin, 0, list->loc_next, index);
    }
}

static void *cx_ll_malloc_node(const cx_linked_list *list) {
    return cxZalloc(list->base.collection.allocator,
                    list->loc_data + list->base.collection.elem_size + list->extra_data_len);
}

static int cx_ll_insert_at(
        struct cx_list_s *list,
        void *node,
        const void *elem
) {
    cx_linked_list *ll = (cx_linked_list *) list;

    // create the new new_node
    void *new_node = cx_ll_malloc_node(ll);

    // sortir if failed
    if (new_node == NULL) return 1;

    // copy the data
    if (elem != NULL) {
        memcpy((char*)new_node + ll->loc_data, elem, list->collection.elem_size);
    }

    // insert
    cx_linked_list_insert_chain(
            &ll->begin, &ll->end,
            ll->loc_prev, ll->loc_next,
            node, new_node, new_node
    );

    // increase the size and return
    list->collection.size++;
    return 0;
}

static size_t cx_ll_insert_array(
        struct cx_list_s *list,
        size_t index,
        const void *array,
        size_t n
) {
    // out-of bounds and corner case check
    if (index > list->collection.size || n == 0) return 0;

    // find position efficiently
    void *node = index == 0 ? NULL : cx_ll_node_at((cx_linked_list *) list, index - 1);

    // perform first insert
    if (0 != cx_ll_insert_at(list, node, array)) return 1;

    // is there more?
    if (n == 1) return 1;

    // we now know exactly where we are
    cx_linked_list *ll = (cx_linked_list *) list;
    node = node == NULL ? ((cx_linked_list *) list)->begin : CX_LL_PTR(node, ll->loc_next);

    // we can add the remaining nodes and immediately advance to the inserted node
    const char *source = array;
    for (size_t i = 1; i < n; i++) {
        if (source != NULL) {
            source += list->collection.elem_size;
        }
        if (0 != cx_ll_insert_at(list, node, source)) return i;
        node = CX_LL_PTR(node, ll->loc_next);
    }
    return n;
}

static void *cx_ll_insert_element(
        struct cx_list_s *list,
        size_t index,
        const void *element
) {
    // out-of-bounds check
    if (index > list->collection.size) return NULL;

    // find position efficiently
    void *node = index == 0 ? NULL : cx_ll_node_at((cx_linked_list *) list, index - 1);

    // perform first insert
    if (cx_ll_insert_at(list, node, element)) return NULL;

    // return a pointer to the data of the inserted node
    cx_linked_list *ll = (cx_linked_list *) list;
    if (node == NULL) {
        return (char*)(ll->begin) + ll->loc_data;
    } else {
        char *next = CX_LL_PTR(node, ll->loc_next);
        return next + ll->loc_data;
    }
}

static _Thread_local cx_compare_func cx_ll_insert_sorted_cmp_func;
static _Thread_local off_t cx_ll_insert_sorted_loc_data;

static int cx_ll_insert_sorted_cmp_helper(const void *l, const void *r) {
    const char *left = (const char*)l + cx_ll_insert_sorted_loc_data;
    const char *right = (const char*)r + cx_ll_insert_sorted_loc_data;
    return cx_ll_insert_sorted_cmp_func(left, right);
}

static size_t cx_ll_insert_sorted_impl(
        struct cx_list_s *list,
        const void *array,
        size_t n,
        bool allow_duplicates
) {
    cx_linked_list *ll = (cx_linked_list *) list;

    // special case
    if (n == 0) return 0;

    // create a new chain of nodes
    void *chain = cx_ll_malloc_node(ll);
    if (chain == NULL) return 0;

    memcpy((char*)chain + ll->loc_data, array, list->collection.elem_size);

    // add all elements from the array to that chain
    void *prev = chain;
    const char *src = array;
    size_t inserted = 1;
    for (; inserted < n; inserted++) {
        void *next = cx_ll_malloc_node(ll);
        if (next == NULL) break;
        src += list->collection.elem_size;
        memcpy((char*)next + ll->loc_data, src, list->collection.elem_size);
        CX_LL_PTR(prev, ll->loc_next) = next;
        CX_LL_PTR(next, ll->loc_prev) = prev;
        prev = next;
    }
    CX_LL_PTR(prev, ll->loc_next) = NULL;

    // invoke the low level function
    cx_ll_insert_sorted_cmp_func = list->collection.cmpfunc;
    cx_ll_insert_sorted_loc_data = ll->loc_data;
    if (allow_duplicates) {
        cx_linked_list_insert_sorted_chain(
                &ll->begin,
                &ll->end,
                ll->loc_prev,
                ll->loc_next,
                chain,
                cx_ll_insert_sorted_cmp_helper
        );
        list->collection.size += inserted;
    } else {
        void *duplicates = cx_linked_list_insert_unique_chain(
                &ll->begin,
                &ll->end,
                ll->loc_prev,
                ll->loc_next,
                chain,
                cx_ll_insert_sorted_cmp_helper
        );
        list->collection.size += inserted;
        // free the nodes that did not make it into the list
        while (duplicates != NULL) {
            void *next = CX_LL_PTR(duplicates, ll->loc_next);
            cxFree(list->collection.allocator, duplicates);
            duplicates = next;
            list->collection.size--;
        }
    }

    return inserted;
}

static size_t cx_ll_insert_sorted(
        struct cx_list_s *list,
        const void *array,
        size_t n
) {
    return cx_ll_insert_sorted_impl(list, array, n, true);
}

static size_t cx_ll_insert_unique(
        struct cx_list_s *list,
        const void *array,
        size_t n
) {
    return cx_ll_insert_sorted_impl(list, array, n, false);
}

static size_t cx_ll_remove(
        struct cx_list_s *list,
        size_t index,
        size_t num,
        void *targetbuf
) {
    cx_linked_list *ll = (cx_linked_list *) list;
    void *node = cx_ll_node_at(ll, index);

    // out-of-bounds check
    if (node == NULL) return 0;

    // remove
    size_t removed = cx_linked_list_remove_chain(
            (void **) &ll->begin,
            (void **) &ll->end,
            ll->loc_prev,
            ll->loc_next,
            node,
            num
    );

    // adjust size
    list->collection.size -= removed;

    // copy or destroy the removed chain
    if (targetbuf == NULL) {
        char *n = node;
        for (size_t i = 0; i < removed; i++) {
            // element destruction
            cx_invoke_destructor(list, n + ll->loc_data);

            // free the node and advance
            void *next = CX_LL_PTR(n, ll->loc_next);
            cxFree(list->collection.allocator, n);
            n = next;
        }
    } else {
        char *dest = targetbuf;
        char *n = node;
        for (size_t i = 0; i < removed; i++) {
            // copy payload
            memcpy(dest, n + ll->loc_data, list->collection.elem_size);

            // advance target buffer
            dest += list->collection.elem_size;

            // free the node and advance
            void *next = CX_LL_PTR(n, ll->loc_next);
            cxFree(list->collection.allocator, n);
            n = next;
        }
    }

    return removed;
}

static void cx_ll_clear(struct cx_list_s *list) {
    if (list->collection.size == 0) return;

    cx_linked_list *ll = (cx_linked_list *) list;
    char *node = ll->begin;
    while (node != NULL) {
        cx_invoke_destructor(list, node + ll->loc_data);
        void *next = CX_LL_PTR(node, ll->loc_next);
        cxFree(list->collection.allocator, node);
        node = next;
    }
    ll->begin = ll->end = NULL;
    list->collection.size = 0;
}

static int cx_ll_swap(
        struct cx_list_s *list,
        size_t i,
        size_t j
) {
    if (i >= list->collection.size || j >= list->collection.size) return 1;
    if (i == j) return 0;

    // perform an optimized search that finds both elements in one run
    cx_linked_list *ll = (cx_linked_list *) list;
    size_t mid = list->collection.size / 2;
    size_t left, right;
    if (i < j) {
        left = i;
        right = j;
    } else {
        left = j;
        right = i;
    }
    void *nleft = NULL, *nright = NULL;
    if (left < mid && right < mid) {
        // case 1: both items left from mid
        nleft = cx_ll_node_at(ll, left);
        assert(nleft != NULL);
        nright = nleft;
        for (size_t c = left; c < right; c++) {
            nright = CX_LL_PTR(nright, ll->loc_next);
        }
    } else if (left >= mid && right >= mid) {
        // case 2: both items right from mid
        nright = cx_ll_node_at(ll, right);
        assert(nright != NULL);
        nleft = nright;
        for (size_t c = right; c > left; c--) {
            nleft = CX_LL_PTR(nleft, ll->loc_prev);
        }
    } else {
        // case 3: one item left, one item right

        // chose the closest to begin / end
        size_t closest;
        size_t other;
        size_t diff2boundary = list->collection.size - right - 1;
        if (left <= diff2boundary) {
            closest = left;
            other = right;
            nleft = cx_ll_node_at(ll, left);
        } else {
            closest = right;
            other = left;
            diff2boundary = left;
            nright = cx_ll_node_at(ll, right);
        }

        // is other element closer to us or closer to boundary?
        if (right - left <= diff2boundary) {
            // search other element starting from already found element
            if (closest == left) {
                nright = nleft;
                for (size_t c = left; c < right; c++) {
                    nright = CX_LL_PTR(nright, ll->loc_next);
                }
            } else {
                nleft = nright;
                for (size_t c = right; c > left; c--) {
                    nleft = CX_LL_PTR(nleft, ll->loc_prev);
                }
            }
        } else {
            // search other element starting at the boundary
            if (closest == left) {
                nright = cx_ll_node_at(ll, other);
            } else {
                nleft = cx_ll_node_at(ll, other);
            }
        }
    }

    void *prev = CX_LL_PTR(nleft, ll->loc_prev);
    void *next = CX_LL_PTR(nright, ll->loc_next);
    void *midstart = CX_LL_PTR(nleft, ll->loc_next);
    void *midend = CX_LL_PTR(nright, ll->loc_prev);

    if (prev == NULL) {
        ll->begin = nright;
    } else {
        CX_LL_PTR(prev, ll->loc_next) = nright;
    }
    CX_LL_PTR(nright, ll->loc_prev) = prev;
    if (midstart == nright) {
        // special case: both nodes are adjacent
        CX_LL_PTR(nright, ll->loc_next) = nleft;
        CX_LL_PTR(nleft, ll->loc_prev) = nright;
    } else {
        // likely case: a chain is between the two nodes
        CX_LL_PTR(nright, ll->loc_next) = midstart;
        CX_LL_PTR(midstart, ll->loc_prev) = nright;
        CX_LL_PTR(midend, ll->loc_next) = nleft;
        CX_LL_PTR(nleft, ll->loc_prev) = midend;
    }
    CX_LL_PTR(nleft, ll->loc_next) = next;
    if (next == NULL) {
        ll->end = nleft;
    } else {
        CX_LL_PTR(next, ll->loc_prev) = nleft;
    }

    return 0;
}

static void *cx_ll_at(
        const struct cx_list_s *list,
        size_t index
) {
    cx_linked_list *ll = (cx_linked_list *) list;
    char *node = cx_ll_node_at(ll, index);
    return node == NULL ? NULL : node + ll->loc_data;
}

static size_t cx_ll_find_remove(
        struct cx_list_s *list,
        const void *elem,
        bool remove
) {
    if (list->collection.size == 0) return 0;

    size_t index;
    cx_linked_list *ll = (cx_linked_list *) list;
    char *node = cx_linked_list_find(
            ll->begin,
            ll->loc_next, ll->loc_data,
            list->collection.cmpfunc, elem,
            &index
    );
    if (node == NULL) {
        return list->collection.size;
    }
    if (remove) {
        cx_invoke_destructor(list, node + ll->loc_data);
        cx_linked_list_remove(&ll->begin, &ll->end,
                              ll->loc_prev, ll->loc_next, node);
        list->collection.size--;
        cxFree(list->collection.allocator, node);
    }
    return index;
}

static void cx_ll_sort(struct cx_list_s *list) {
    cx_linked_list *ll = (cx_linked_list *) list;
    cx_linked_list_sort(&ll->begin, &ll->end,
                        ll->loc_prev, ll->loc_next, ll->loc_data,
                        list->collection.cmpfunc);
}

static void cx_ll_reverse(struct cx_list_s *list) {
    cx_linked_list *ll = (cx_linked_list *) list;
    cx_linked_list_reverse(&ll->begin, &ll->end, ll->loc_prev, ll->loc_next);
}

static int cx_ll_compare(
        const struct cx_list_s *list,
        const struct cx_list_s *other
) {
    cx_linked_list *left = (cx_linked_list *) list;
    cx_linked_list *right = (cx_linked_list *) other;
    assert(left->loc_next == right->loc_next);
    assert(left->loc_data == right->loc_data);
    return cx_linked_list_compare(left->begin, right->begin,
                                  left->loc_next, left->loc_data,
                                  list->collection.cmpfunc);
}

static bool cx_ll_iter_valid(const void *it) {
    const struct cx_iterator_s *iter = it;
    return iter->elem_handle != NULL;
}

static void cx_ll_iter_next(void *it) {
    struct cx_iterator_s *iter = it;
    cx_linked_list *ll = iter->src_handle;
    if (iter->base.remove) {
        iter->base.remove = false;
        struct cx_list_s *list = iter->src_handle;
        char *node = iter->elem_handle;
        iter->elem_handle = CX_LL_PTR(node, ll->loc_next);
        cx_invoke_destructor(list, node + ll->loc_data);
        cx_linked_list_remove(&ll->begin, &ll->end,
                              ll->loc_prev, ll->loc_next, node);
        list->collection.size--;
        iter->elem_count--;
        cxFree(list->collection.allocator, node);
    } else {
        iter->index++;
        void *node = iter->elem_handle;
        iter->elem_handle = CX_LL_PTR(node, ll->loc_next);
    }
}

static void cx_ll_iter_prev(void *it) {
    struct cx_iterator_s *iter = it;
    cx_linked_list *ll = iter->src_handle;
    if (iter->base.remove) {
        iter->base.remove = false;
        struct cx_list_s *list = iter->src_handle;
        char *node = iter->elem_handle;
        iter->elem_handle = CX_LL_PTR(node, ll->loc_prev);
        iter->index--;
        cx_invoke_destructor(list, node + ll->loc_data);
        cx_linked_list_remove(&ll->begin, &ll->end,
                              ll->loc_prev, ll->loc_next, node);
        list->collection.size--;
        iter->elem_count--;
        cxFree(list->collection.allocator, node);
    } else {
        iter->index--;
        char *node = iter->elem_handle;
        iter->elem_handle = CX_LL_PTR(node, ll->loc_prev);
    }
}

static void *cx_ll_iter_current(const void *it) {
    const struct cx_iterator_s *iter = it;
    const cx_linked_list *ll = iter->src_handle;
    char *node = iter->elem_handle;
    return node + ll->loc_data;
}

static CxIterator cx_ll_iterator(
        const struct cx_list_s *list,
        size_t index,
        bool backwards
) {
    CxIterator iter;
    iter.index = index;
    iter.src_handle = (void*)list;
    iter.elem_handle = cx_ll_node_at((const cx_linked_list *) list, index);
    iter.elem_size = list->collection.elem_size;
    iter.elem_count = list->collection.size;
    iter.base.valid = cx_ll_iter_valid;
    iter.base.current = cx_ll_iter_current;
    iter.base.next = backwards ? cx_ll_iter_prev : cx_ll_iter_next;
    iter.base.allow_remove = true;
    iter.base.remove = false;
    return iter;
}

static int cx_ll_insert_iter(
        CxIterator *iter,
        const void *elem,
        int prepend
) {
    struct cx_list_s *list = iter->src_handle;
    cx_linked_list *ll = iter->src_handle;
    void *node = iter->elem_handle;
    if (node != NULL) {
        assert(prepend >= 0 && prepend <= 1);
        void *choice[2] = {node, CX_LL_PTR(node, ll->loc_prev)};
        int result = cx_ll_insert_at(list, choice[prepend], elem);
        if (result == 0) {
            iter->elem_count++;
            if (prepend) {
                iter->index++;
            }
        }
        return result;
    } else {
        if (cx_ll_insert_element(list, list->collection.size, elem) == NULL) {
            return 1; // LCOV_EXCL_LINE
        }
        iter->elem_count++;
        iter->index = list->collection.size;
        return 0;
    }
}

static void cx_ll_destructor(CxList *list) {
    cx_linked_list *ll = (cx_linked_list *) list;

    char *node = ll->begin;
    while (node) {
        cx_invoke_destructor(list, node + ll->loc_data);
        void *next = CX_LL_PTR(node, ll->loc_next);
        cxFree(list->collection.allocator, node);
        node = next;
    }

    cxFree(list->collection.allocator, list);
}

static cx_list_class cx_linked_list_class = {
        cx_ll_destructor,
        cx_ll_insert_element,
        cx_ll_insert_array,
        cx_ll_insert_sorted,
        cx_ll_insert_unique,
        cx_ll_insert_iter,
        cx_ll_remove,
        cx_ll_clear,
        cx_ll_swap,
        cx_ll_at,
        cx_ll_find_remove,
        cx_ll_sort,
        cx_ll_compare,
        cx_ll_reverse,
        NULL, // no overallocation supported
        cx_ll_iterator,
};

CxList *cxLinkedListCreate(
        const CxAllocator *allocator,
        cx_compare_func comparator,
        size_t elem_size
) {
    if (allocator == NULL) {
        allocator = cxDefaultAllocator;
    }

    cx_linked_list *list = cxCalloc(allocator, 1, sizeof(cx_linked_list));
    if (list == NULL) return NULL;
    list->extra_data_len = 0;
    list->loc_prev = 0;
    list->loc_next = sizeof(void*);
    list->loc_data = sizeof(void*)*2;
    cx_list_init((CxList*)list, &cx_linked_list_class,
            allocator, comparator, elem_size);

    return (CxList *) list;
}

mercurial