reactos/sdk/lib/skiplist/skiplist.c
2021-06-11 15:33:08 +03:00

441 lines
16 KiB
C

/*
* PROJECT: Skiplist implementation for the ReactOS Project
* LICENSE: GPL-2.0+ (https://spdx.org/licenses/GPL-2.0+)
* PURPOSE: All implemented functions operating on the Skiplist
* COPYRIGHT: Copyright 2015 Colin Finck (colin@reactos.org)
*/
#include <intrin.h>
#include <windef.h>
#include <winbase.h>
#include "skiplist.h"
/**
* @name _GetRandomLevel
*
* Returns a random level for the next element to be inserted.
* This level is geometrically distributed for p = 0.5, so perfectly suitable for an efficient Skiplist implementation.
*
* @return
* A value between 0 and SKIPLIST_LEVELS - 1.
*/
static __inline CHAR
_GetRandomLevel()
{
// Using a simple fixed seed and the Park-Miller Lehmer Minimal Standard Random Number Generator gives an acceptable distribution for our "random" levels.
static DWORD dwRandom = 1;
DWORD dwLevel = 0;
DWORD dwShifted;
// Generate 31 uniformly distributed pseudo-random bits using the Park-Miller Lehmer Minimal Standard Random Number Generator.
dwRandom = (DWORD)(((ULONGLONG)dwRandom * 48271UL) % 2147483647UL);
// Shift out (31 - SKIPLIST_LEVELS) bits to the right to have no more than SKIPLIST_LEVELS bits set.
dwShifted = dwRandom >> (31 - SKIPLIST_LEVELS);
// BitScanForward doesn't operate on a zero input value.
if (dwShifted)
{
// BitScanForward sets dwLevel to the zero-based position of the first set bit (from LSB to MSB).
// This makes dwLevel a geometrically distributed value between 0 and SKIPLIST_LEVELS - 1 for p = 0.5.
BitScanForward(&dwLevel, dwShifted);
}
// dwLevel can't have a value higher than 30 this way, so a CHAR is more than enough.
return (CHAR)dwLevel;
}
/**
* @name _InsertElementSkiplistWithInformation
*
* Determines a level for the new element and inserts it at the given position in the Skiplist.
* This function is internally used by the Skiplist insertion functions.
*
* @param Skiplist
* Pointer to the SKIPLIST structure to operate on.
*
* @param Element
* The element to insert.
*
* @param pUpdate
* Array containing the last nodes before our new node on each level.
*
* @param dwDistance
* Array containing the distance to the last node before our new node on each level.
*
* @return
* TRUE if the node was successfully inserted, FALSE if no memory could be allocated for it.
*/
static BOOL
_InsertElementSkiplistWithInformation(PSKIPLIST Skiplist, PVOID Element, PSKIPLIST_NODE* pUpdate, DWORD* dwDistance)
{
CHAR chNewLevel;
CHAR i;
PSKIPLIST_NODE pNode;
// Get the highest level, on which the node shall be inserted.
chNewLevel = _GetRandomLevel();
// Check if the new level is higher than the maximum level we currently have in the Skiplist.
if (chNewLevel > Skiplist->MaximumLevel)
{
// It is, so we also need to insert the new node right after the Head node on some levels.
// These are the levels higher than the current maximum level up to the new level.
// We also need to set the distance of these elements to the new node count to account for the calculations below.
for (i = Skiplist->MaximumLevel + 1; i <= chNewLevel; i++)
{
pUpdate[i] = &Skiplist->Head;
pUpdate[i]->Distance[i] = Skiplist->NodeCount + 1;
}
// The new level is the new maximum level of the entire Skiplist.
Skiplist->MaximumLevel = chNewLevel;
}
// Finally create our new Skiplist node.
pNode = Skiplist->AllocateRoutine(sizeof(SKIPLIST_NODE));
if (!pNode)
return FALSE;
pNode->Element = Element;
// For each used level, insert us between the saved node for this level and its current next node.
for (i = 0; i <= chNewLevel; i++)
{
pNode->Next[i] = pUpdate[i]->Next[i];
pUpdate[i]->Next[i] = pNode;
// We know the walked distance in this level: dwDistance[i]
// We also know the element index of the new node: dwDistance[0]
// The new node's distance is now the walked distance in this level plus the difference between the saved node's distance and the element index.
pNode->Distance[i] = dwDistance[i] + (pUpdate[i]->Distance[i] - dwDistance[0]);
// The saved node's distance is now the element index plus one (to account for the added node) minus the walked distance in this level.
pUpdate[i]->Distance[i] = dwDistance[0] + 1 - dwDistance[i];
}
// For all levels above the new node's level, we need to increment the distance, because we've just added our new node.
for (i = chNewLevel + 1; i <= Skiplist->MaximumLevel; i++)
++pUpdate[i]->Distance[i];
// We've successfully added a node :)
++Skiplist->NodeCount;
return TRUE;
}
/**
* @name DeleteElementSkiplist
*
* Deletes an element from the Skiplist. The efficiency of this operation is O(log N) on average.
*
* Instead of the result of a LookupElementSkiplist call, it's sufficient to provide a dummy element with just enough information for your CompareRoutine.
* A lookup for the element to be deleted needs to be performed in any case.
*
* @param Skiplist
* Pointer to the SKIPLIST structure to operate on.
*
* @param Element
* Information about the element to be deleted.
*
* @return
* Returns the deleted element or NULL if no such element was found.
* You can then free memory for the deleted element if necessary.
*/
PVOID
DeleteElementSkiplist(PSKIPLIST Skiplist, PVOID Element)
{
CHAR i;
PSKIPLIST_NODE pLastComparedNode = NULL;
PSKIPLIST_NODE pNode = &Skiplist->Head;
PSKIPLIST_NODE pUpdate[SKIPLIST_LEVELS];
PVOID pReturnValue;
// Find the node on every currently used level, after which the node to be deleted must follow.
// This can be done efficiently by starting from the maximum level and going down a level each time a position has been found.
for (i = Skiplist->MaximumLevel + 1; --i >= 0;)
{
while (pNode->Next[i] && pNode->Next[i] != pLastComparedNode && Skiplist->CompareRoutine(pNode->Next[i]->Element, Element) < 0)
pNode = pNode->Next[i];
// Reduce the number of comparisons by not comparing the same node on different levels twice.
pLastComparedNode = pNode->Next[i];
pUpdate[i] = pNode;
}
// Check if the node we're looking for has been found.
pNode = pNode->Next[0];
if (!pNode || Skiplist->CompareRoutine(pNode->Element, Element) != 0)
{
// It hasn't been found, so there's nothing to delete.
return NULL;
}
// Beginning at the lowest level, remove the node from each level of the list and merge distances.
// We can stop as soon as we found the first level that doesn't contain the node.
for (i = 0; i <= Skiplist->MaximumLevel && pUpdate[i]->Next[i] == pNode; i++)
{
pUpdate[i]->Distance[i] += pNode->Distance[i] - 1;
pUpdate[i]->Next[i] = pNode->Next[i];
}
// Now decrement the distance of the corresponding node in levels higher than the deleted node's level to account for the deleted node.
while (i <= Skiplist->MaximumLevel)
{
--pUpdate[i]->Distance[i];
i++;
}
// Return the deleted element (so the caller can free it if necessary) and free the memory for the node itself (allocated by us).
pReturnValue = pNode->Element;
Skiplist->FreeRoutine(pNode);
// Find all levels which now contain no more nodes and reduce the maximum level of the entire Skiplist accordingly.
while (Skiplist->MaximumLevel > 0 && !Skiplist->Head.Next[Skiplist->MaximumLevel])
--Skiplist->MaximumLevel;
// We've successfully deleted the node :)
--Skiplist->NodeCount;
return pReturnValue;
}
/**
* @name InitializeSkiplist
*
* Initializes a new Skiplist structure.
*
* @param Skiplist
* Pointer to the SKIPLIST structure to operate on.
*
* @param AllocateRoutine
* Pointer to a SKIPLIST_ALLOCATE_ROUTINE for allocating memory for new Skiplist nodes.
*
* @param CompareRoutine
* Pointer to a SKIPLIST_COMPARE_ROUTINE for comparing two elements of the Skiplist.
*
* @param FreeRoutine
* Pointer to a SKIPLIST_FREE_ROUTINE for freeing memory allocated with AllocateRoutine.
*/
void
InitializeSkiplist(PSKIPLIST Skiplist, PSKIPLIST_ALLOCATE_ROUTINE AllocateRoutine, PSKIPLIST_COMPARE_ROUTINE CompareRoutine, PSKIPLIST_FREE_ROUTINE FreeRoutine)
{
// Store the routines.
Skiplist->AllocateRoutine = AllocateRoutine;
Skiplist->CompareRoutine = CompareRoutine;
Skiplist->FreeRoutine = FreeRoutine;
// Initialize the members and pointers.
// The Distance array is only used when a node is non-NULL, so it doesn't need initialization.
Skiplist->MaximumLevel = 0;
Skiplist->NodeCount = 0;
ZeroMemory(Skiplist->Head.Next, sizeof(Skiplist->Head.Next));
}
/**
* @name InsertElementSkiplist
*
* Inserts a new element into the Skiplist. The efficiency of this operation is O(log N) on average.
* Uses CompareRoutine to find the right position for the insertion.
*
* @param Skiplist
* Pointer to the SKIPLIST structure to operate on.
*
* @param Element
* The element to insert.
*
* @return
* TRUE if the node was successfully inserted, FALSE if it already exists or no memory could be allocated for it.
*/
BOOL
InsertElementSkiplist(PSKIPLIST Skiplist, PVOID Element)
{
CHAR i;
DWORD dwDistance[SKIPLIST_LEVELS + 1] = { 0 };
PSKIPLIST_NODE pLastComparedNode = NULL;
PSKIPLIST_NODE pNode = &Skiplist->Head;
PSKIPLIST_NODE pUpdate[SKIPLIST_LEVELS];
// Find the node on every currently used level, after which the new node needs to be inserted.
// This can be done efficiently by starting from the maximum level and going down a level each time a position has been found.
for (i = Skiplist->MaximumLevel + 1; --i >= 0;)
{
// When entering this level, we begin at the distance of the last level we walked through.
dwDistance[i] = dwDistance[i + 1];
while (pNode->Next[i] && pNode->Next[i] != pLastComparedNode && Skiplist->CompareRoutine(pNode->Next[i]->Element, Element) < 0)
{
// Save our position in every level when walking through the nodes.
dwDistance[i] += pNode->Distance[i];
// Advance to the next node.
pNode = pNode->Next[i];
}
// Reduce the number of comparisons by not comparing the same node on different levels twice.
pLastComparedNode = pNode->Next[i];
pUpdate[i] = pNode;
}
// Check if the node already exists in the Skiplist.
pNode = pNode->Next[0];
if (pNode && Skiplist->CompareRoutine(pNode->Element, Element) == 0)
{
// All elements to be inserted mustn't exist in the list, so we see this as a failure.
return FALSE;
}
// The rest of the procedure is the same for both insertion functions.
return _InsertElementSkiplistWithInformation(Skiplist, Element, pUpdate, dwDistance);
}
/**
* @name InsertTailElementSkiplist
*
* Inserts a new element at the end of the Skiplist. The efficiency of this operation is O(log N) on average.
* In contrast to InsertElementSkiplist, this function is more efficient by not calling CompareRoutine at all and always inserting the element at the end.
* You're responsible for calling this function only when you can guarantee that InsertElementSkiplist would also insert the element at the end.
*
* @param Skiplist
* Pointer to the SKIPLIST structure to operate on.
*
* @param Element
* The element to insert.
*
* @return
* TRUE if the node was successfully inserted, FALSE if it already exists or no memory could be allocated for it.
*/
BOOL
InsertTailElementSkiplist(PSKIPLIST Skiplist, PVOID Element)
{
CHAR i;
DWORD dwDistance[SKIPLIST_LEVELS + 1] = { 0 };
PSKIPLIST_NODE pNode = &Skiplist->Head;
PSKIPLIST_NODE pUpdate[SKIPLIST_LEVELS];
// Find the last node on every currently used level, after which the new node needs to be inserted.
// This can be done efficiently by starting from the maximum level and going down a level each time a position has been found.
for (i = Skiplist->MaximumLevel + 1; --i >= 0;)
{
// When entering this level, we begin at the distance of the last level we walked through.
dwDistance[i] = dwDistance[i + 1];
while (pNode->Next[i])
{
// Save our position in every level when walking through the nodes.
dwDistance[i] += pNode->Distance[i];
// Advance to the next node.
pNode = pNode->Next[i];
}
pUpdate[i] = pNode;
}
// The rest of the procedure is the same for both insertion functions.
return _InsertElementSkiplistWithInformation(Skiplist, Element, pUpdate, dwDistance);
}
/**
* @name LookupElementSkiplist
*
* Looks up an element in the Skiplist. The efficiency of this operation is O(log N) on average.
*
* @param Skiplist
* Pointer to the SKIPLIST structure to operate on.
*
* @param Element
* Information about the element to look for.
*
* @param ElementIndex
* Pointer to a DWORD that will contain the zero-based index of the element in the Skiplist.
* If you're not interested in the index, you can set this parameter to NULL.
*
* @return
* Returns the found element or NULL if no such element was found.
*/
PVOID
LookupElementSkiplist(PSKIPLIST Skiplist, PVOID Element, PDWORD ElementIndex)
{
CHAR i;
DWORD dwIndex = 0;
PSKIPLIST_NODE pLastComparedNode = NULL;
PSKIPLIST_NODE pNode = &Skiplist->Head;
// Do the efficient lookup in Skiplists:
// * Start from the maximum level.
// * Walk through all nodes on this level that come before the node we're looking for.
// * When we have reached such a node, go down a level and continue there.
// * Repeat these steps till we're in level 0, right in front of the node we're looking for.
for (i = Skiplist->MaximumLevel + 1; --i >= 0;)
{
while (pNode->Next[i] && pNode->Next[i] != pLastComparedNode && Skiplist->CompareRoutine(pNode->Next[i]->Element, Element) < 0)
{
dwIndex += pNode->Distance[i];
pNode = pNode->Next[i];
}
// Reduce the number of comparisons by not comparing the same node on different levels twice.
pLastComparedNode = pNode->Next[i];
}
// We must be right in front of the node we're looking for now, otherwise it doesn't exist in the Skiplist at all.
pNode = pNode->Next[0];
if (!pNode || Skiplist->CompareRoutine(pNode->Element, Element) != 0)
{
// It hasn't been found, so there's nothing to return.
return NULL;
}
// Return the index of the element if the caller is interested.
if (ElementIndex)
*ElementIndex = dwIndex;
// Return the stored element of the found node.
return pNode->Element;
}
/**
* @name LookupNodeByIndexSkiplist
*
* Looks up a node in the Skiplist at the given position. The efficiency of this operation is O(log N) on average.
*
* @param Skiplist
* Pointer to the SKIPLIST structure to operate on.
*
* @param ElementIndex
* Zero-based position of the node in the Skiplist.
*
* @return
* Returns the found node or NULL if the position is invalid.
*/
PSKIPLIST_NODE
LookupNodeByIndexSkiplist(PSKIPLIST Skiplist, DWORD ElementIndex)
{
CHAR i;
DWORD dwIndex = 0;
PSKIPLIST_NODE pNode = &Skiplist->Head;
// The only way the node can't be found is when the index is out of range.
if (ElementIndex >= Skiplist->NodeCount)
return NULL;
// Do the efficient lookup in Skiplists:
// * Start from the maximum level.
// * Walk through all nodes on this level that come before the node we're looking for.
// * When we have reached such a node, go down a level and continue there.
// * Repeat these steps till we're in level 0, right in front of the node we're looking for.
for (i = Skiplist->MaximumLevel + 1; --i >= 0;)
{
// We compare with <= instead of < here, because the added distances make up a 1-based index while ElementIndex is zero-based,
// so we have to jump one node further.
while (pNode->Next[i] && dwIndex + pNode->Distance[i] <= ElementIndex)
{
dwIndex += pNode->Distance[i];
pNode = pNode->Next[i];
}
}
// We are right in front of the node we're looking for now.
return pNode->Next[0];
}