Chapter 4
Chapter 4
In a singly linked list, each node points to the next node in the list, and the last node points to
NULL, indicating the end of the list.
1. Insertion: Adding a new node at the beginning, end, or at a specific position in the list.
2. Deletion: Removing a node from the beginning, end, or a specific position in the list.
3. Traversal: Accessing each node in the list to read or display data.
4. Search: Finding a node with a specific value.
#include <iostream>
struct Node {
};
// Define the SinglyLinkedList class
class SinglyLinkedList {
private:
public:
head = newNode;
return;
temp = temp->next;
temp = temp->next;
if (temp == NULL) {
delete newNode;
return;
}
// Delete a node from the beginning
void deleteFromBeginning() {
if (head == NULL) {
return;
void deleteFromEnd() {
if (head == NULL) {
return;
if (head->next == NULL) {
delete head;
head = NULL;
return;
temp = temp->next;
delete temp->next;
temp->next = NULL;
if (head == NULL) {
return;
if (head->data == value) {
head = head->next;
delete temp;
return;
temp = temp->next;
}
if (temp->next == NULL) {
return;
temp->next = nodeToDelete->next;
delete nodeToDelete;
void display() {
temp = temp->next;
~SinglyLinkedList() {
temp = nextNode;
};
int main() {
SinglyLinkedList list;
// Insert nodes
list.insertAtBeginning(10);
list.insertAtEnd(20);
list.insertAtEnd(30);
list.display();
// Delete nodes
list.deleteFromBeginning();
list.display();
list.deleteFromEnd();
list.display();
list.deleteByValue(25);
list.display();
return 0;
In a doubly linked list, each node points to both its next and previous nodes, allowing traversal in
both directions (forward and backward).
Here’s a complete C++ example that demonstrates the basic operations for a doubly linked list.
#include <iostream>
using namespace std;
public:
// Constructor to initialize an empty list
DoublyLinkedList() : head(NULL), tail(NULL) {}
int main() {
DoublyLinkedList list;
// Insert nodes
list.insertAtBeginning(10);
list.insertAtEnd(20);
list.insertAtEnd(30);
list.insertAtBeginning(5);
// Delete nodes
list.deleteFromBeginning();
cout << "List after deleting from beginning: ";
list.displayForward();
list.deleteFromEnd();
cout << "List after deleting from end: ";
list.displayForward();
return 0;
}
Circular lists
A circular list is a variation of a linked list in which the last node points back to the first node,
creating a circular structure with no beginning or end. Circular linked lists can be either singly or
doubly linked, but here, we will focus on the singly linked circular list.
1. The last node’s next pointer points back to the first node, creating a loop.
2. There is no NULL pointer in the list, unlike a traditional singly linked list.
3. Traversing a circular linked list requires special handling to avoid infinite loops, as there
is no natural endpoint (NULL).
Efficient Circular Traversal: Useful when you need to loop through data continuously,
such as in round-robin scheduling.
Constant Time Insertions: Adding or removing elements from both ends of the list can
be done in constant time, provided you maintain a pointer to the last node.
Let’s create a simple circular linked list with the following operations:
Code Implementation
#include <iostream>
using namespace std;
class Node {
public:
int data; // Data part of the node
Node* next; // Pointer to the next node
Node(int value) {
data = value;
next = NULL;
}
};
public:
// Constructor to initialize an empty list
CircularLinkedList() {
last = NULL;
}
// Function to insert a new node at the end of the circular linked list
void insertAtEnd(int value) {
Node* newNode = new Node(value); // Create a new node
if (last == NULL) {
// If the list is empty, make the new node point to itself and
set it as last
last = newNode;
last->next = last;
} else {
// Insert the new node after the last node and update pointers
newNode->next = last->next; // New node points to the first
node
last->next = newNode; // Last node points to the new
node
last = newNode; // Update last to the new node
}
}
do {
nextNode = current->next; // Save the next node
delete current; // Delete the current node
current = nextNode; // Move to the next node
} while (current != last->next); // Stop when back to the first
node
int main() {
CircularLinkedList list;
return 0;
}
Skip Lists
A skip list is a probabilistic data structure that allows for fast search, insertion, and deletion
operations. It is an alternative to balanced trees, providing a simpler and often more efficient way
to maintain a sorted list of elements. Skip lists achieve logarithmic time complexity for these
operations through a layered structure that allows for "skipping" over elements.
The bottom level contains all the elements sorted in increasing order.
Each higher level acts as an "express lane" for the level below it, containing a subset of the
elements from the level directly below.
Each element in a higher level has a pointer to a subsequent element in the same level and also
points to elements in the lower level.
How Skip Lists Work
1. Levels: A skip list has multiple levels (or layers). The lowest level contains all the
elements, while each subsequent level contains fewer elements. The probability of an
element being included in a higher level is typically set at 50%.
2. Insertion: When a new element is inserted:
o It is added to the bottom level first.
o Then, a random number of pointers are created for the new element, which may place it
into higher levels.
o The number of levels is determined randomly, often using a coin flip.
3. Search: Searching for an element starts at the highest level and moves horizontally
across pointers until the target value is greater than or less than the current element. If the
value is less than the current element, the search moves down to the next level. This
continues until the element is found or the search reaches the bottom level.
4. Deletion: Deletion works similarly to insertion. The element is located and removed from
all levels where it appears.
rust
Copy code
Level 3: |----> 7 ----> 20 ----> 50 ---->
Level 2: |--> 3 --> 7 --> 15 --> 20 --> 30 --> 50 --> 60
Level 1: |--> 1 --> 2 --> 3 --> 4 --> 5 --> 7 --> 10 --> 15 --> 20 -->
30 --> 50 --> 60
Level 0: 1 2 3 4 5 6 7 8 9 10 15 20 30 50 60
Operations
Insertion
To insert a value:
1. Start from the highest level and move right until you find a value greater than the value to be
inserted.
2. Move down to the next level and repeat until you reach the bottom level.
3. Insert the new value at the bottom level.
4. Randomly determine how many levels the new value will occupy above the bottom level and
insert it into those levels.
Search
Deletion
To delete a value:
#include <iostream>
#include <cstdlib>
#include <vector>
#include <ctime>
class Node {
public:
int value;
this->value = value;
};
class SkipList {
private:
public:
int randomLevel() {
int level = 0;
level++;
}
return level;
// Start from the highest level and find the position to insert
current = current->forward[i];
current = current->forward[0];
currentLevel = newLevel;
newNode->forward[i] = update[i]->forward[i];
void display() {
node = node->forward[i];
}
~SkipList() {
};
int main() {
skipList.insert(3);
skipList.insert(6);
skipList.insert(7);
skipList.insert(9);
skipList.insert(12);
skipList.insert(19);
skipList.insert(17);
skipList.insert(26);
skipList.insert(21);
skipList.insert(25);
skipList.display();
return 0;
1. Node Class: Represents a node in the skip list, containing the value and a vector of
forward pointers to support multiple levels.
2. SkipList Class:
o maxLevel: The maximum number of levels in the skip list.
o p: Probability factor for generating levels.
o header: The header node that acts as a starting point for all levels.
o currentLevel: Tracks the current highest level in the list.
o randomLevel(): Generates a random level for a new node based on the probability p.
o insert(int value): Inserts a new value into the skip list, adjusting pointers and levels as
necessary.
o display(): Outputs the elements in each level of the skip list.
o Destructor: Cleans up memory used by the skip list.
3. Main Function:
o Initializes the skip list and inserts several values.
o Displays the skip list.
Output
The output will show the elements at each level of the skip list. Due to the randomness of the
level generation, the exact output will vary each time you run the program.
Self-organizing lists
Self-organizing lists are dynamic data structures that adapt their ordering based on access
patterns, allowing frequently accessed elements to be moved closer to the front of the list. This
optimization helps reduce access times for commonly used elements. The basic idea is to
rearrange the elements of the list upon access (search), effectively making the most frequently
accessed items quicker to reach in subsequent operations.
Common Strategies
1. Move-To-Front: When an element is accessed, it is moved to the front of the list. This
strategy is effective if there are a few frequently accessed elements.
2. Frequency-Based: Elements are ordered based on how frequently they are accessed.
More frequently accessed elements are moved toward the front based on their access
count.
#include <iostream>
#include <string>
class SelfOrganizingList {
private:
void resize() {
capacity *= 2;
newElements[i] = elements[i];
delete[] elements;
elements = newElements;
public:
SelfOrganizingList() {
size = 0;
~SelfOrganizingList() {
delete[] elements;
if (elements[i] == element) {
index = i;
break;
if (index != -1) {
} else {
size++;
}
void display() {
};
int main() {
SelfOrganizingList sol;
// Access elements
sol.access("grape");
sol.access("apple");
sol.access("banana");
sol.access("orange");
sol.display();
return 0;
1. Dynamic Array:
o We use a dynamic array (string* elements) to store the elements. This allows
us to manage memory manually, simulating the behavior of older C++ standards.
o size keeps track of the current number of elements, while capacity represents
the maximum number of elements the array can currently hold.
2. Resize Function:
o The resize function doubles the capacity of the array when needed. It creates a
new array, copies existing elements, and deletes the old array.
3. Access Function:
o The access function checks if the element exists in the array.
o If it does, it moves that element to the front by shifting the other elements down.
o If the element does not exist, it inserts it at the front, shifting the rest of the
elements accordingly.
4. Display Function: This function outputs the current order of elements in the list.
5. Destructor: Cleans up dynamically allocated memory when the SelfOrganizingList
object is destroyed.
6. Main Function: Demonstrates the usage of the SelfOrganizingList class by accessing
a series of elements and displaying the resulting order.
Conclusion
Self-organizing lists are useful data structures for optimizing access times based on usage
patterns. The Move-To-Front strategy is a simple and effective way to implement this concept.
By utilizing lists and hash maps, we can efficiently manage and access elements, adapting the
structure dynamically based on real-time usage. This makes self-organizing lists a powerful tool
in scenarios where certain elements are accessed more frequently than others.
Sparse Tables
Sparse Tables are a powerful data structure primarily used for answering range queries
efficiently, especially for idempotent operations, where applying the operation multiple
times doesn't change the result (e.g., minimum, maximum, greatest common divisor).
Sparse Tables are widely used for range minimum/maximum queries (RMQ) due to their
efficiency and ease of implementation.
1. Preprocessing: The Sparse Table is built in O(n log n) time. The preprocessing involves
filling a table that can answer queries about minimum (or maximum) values in
logarithmic range segments of the array.
2. Query Time: After preprocessing, the Sparse Table can answer range minimum queries
in O(1) time.
3. Space Complexity: The space complexity is O(n log n), as we store multiple segments of
the array.
Given an array arr, a Sparse Table st is created such that st[i][j] holds the minimum value in
the subarray starting at index i and covering the next 2^j elements.
Formula:
1. Base Case: st[i][j]=arr[i]
2. Recursive Case: For each cell, compute :st[i][j] = min(st[i][j−1], st[i+2(j−1)][j−1])
This means that to compute the minimum of length 2^j starting at i, we use two
overlapping subarrays of length 2^{(j - 1)}.
For a range query [L, R], find the largest power of two that fits into the range and use
it.
Example
Construction
Initialization:
o st[i][0] is initialized to arr[i] for all i.
Filling the Sparse Table:
o For j>0 , compute the minimum for segments of length 2^j based on previously
computed values.
Explanation:
Step 2: Querying
#include <iostream>
#include <cmath>
#include <vector>
class SparseTable {
private:
public:
arr = input;
n = arr.size();
st.resize(n, vector<int>(logn));
st[i][0] = arr[i];
};
int main() {
vector<int> arr;
arr.push_back(1);
arr.push_back(3);
arr.push_back(2);
arr.push_back(7);
arr.push_back(9);
arr.push_back(11);
arr.push_back(3);
arr.push_back(5);
SparseTable st(arr);
cout << "Minimum value in range [1, 5]: " << st.query(1, 5) << endl; //
Output: 2
cout << "Minimum value in range [0, 3]: " << st.query(0, 3) << endl; //
Output: 1
cout << "Minimum value in range [4, 7]: " << st.query(4, 7) << endl; //
Output: 3
return 0;
1. Initialization:
o The constructor initializes the Sparse Table based on the input array and calculates the
minimum values for each segment of lengths 2j2^j2j.
2. Query Function:
o The query method takes two indices, LLL and RRR, and computes the minimum value in
the range by comparing the appropriate segments in the Sparse Table.
Conclusion
Sparse Tables are an efficient solution for static range query problems, particularly for range
minimum or maximum queries. They provide a way to preprocess the data for very fast queries,
making them suitable for situations where data does not change frequently.