1.
Introduction to Doubly Linked Lists
What is a Doubly Linked List?
A Doubly Linked List is a linear data structure where elements (nodes) are stored in
sequence, but unlike singly linked lists, each node contains pointers to both the next
and previous nodes. Each node contains:
● Data: The actual value
● Next Pointer: Reference to the next node in the sequence
● Previous Pointer: Reference to the previous node in the sequence
Advantages over Singly Linked Lists:
● Bidirectional Traversal: Can traverse in both forward and backward directions
● Efficient Deletion: O(1) deletion when node reference is given
● Better for certain algorithms: Some algorithms benefit from backward traversal
● Easier Implementation: Some operations like deletion become simpler
Disadvantages:
● Extra Memory: Each node requires additional memory for previous pointer
● More Complex: Requires maintaining two pointers per node
● Higher Overhead: More pointer operations needed for insertions/deletions
Advantages over Arrays:
● Dynamic Size: Can grow/shrink during runtime
● Efficient Insertion/Deletion: O(1) at beginning and end (with tail pointer)
● Memory Efficient: Only allocates memory as needed
Disadvantages:
● No Random Access: Cannot directly access element at index i
● Extra Memory: Each node requires additional memory for two pointers
● Cache Performance: Poor cache locality due to non-contiguous memory
2. Basic Structure
class Node {
public:
int data; // Data part
Node* next; // Pointer to next node
Node* prev; // Pointer to previous node
Node(int data) {
this->data = data;
this->next = nullptr;
this->prev = nullptr;
}
};
class DoublyLinkedList {
private:
Node* head; // Pointer to first node
public:
DoublyLinkedList() {
head = nullptr;
tail = nullptr;
}
// Methods...
};
3. Basic Operations with Approaches
3.1 INSERTION OPERATIONS
a.) INSERT AT BEGINNING
Theory: Add a new node at the start of the list and update head pointer
Approach:
1. Create new node with given data
2. Set new node next to current head
3. If list is not empty, set current head's prev to new node
4. Update head to point to new node
Time Complexity: O(1) - Only updating few pointers, no traversal needed
Space Complexity: O(1) - Only creating one new node
void insertAtBeginning(int data) {
Node* newNode = new Node(data);
newNode->next = head;
if (head != nullptr) {
head->prev = newNode;
}
head = newNode;
cout << "Inserted: " << data << " at beginning" << endl;
}
b.) INSERT AT END
Theory: Add a new node at the end of the list
Approach:
1. Create new node with given data
2. If list is empty, make new node the head
3. Otherwise, traverse to the last node
4. Set last node's next to new node
5. Set new node's prev to last node
Time Complexity: O(n) - Need to traverse to end without tail pointer
Space Complexity: O(1) - Only creating one new node
void insertAtEnd(int data) {
Node* newNode = new Node(data);
if (head == nullptr) {
head = newNode;
cout << "Inserted: " << data << " at end (first node)" << endl;
return;
}
// Traverse to the end
Node* current = head;
while (current->next != nullptr) {
current = current->next;
}
current->next = newNode;
newNode->prev = current;
cout << "Inserted " << data << " at end" << endl;
}
c.) INSERT AT POSITION
Theory: Add a new node at a specific position (0-indexed)
Approach:
1. Handle edge case: position 0 (insert at beginning)
2. Traverse to the target position
3. Create new node and adjust four pointers:
- new->next, new->prev, prev->next, next->prev
4. Handle special cases: insert at end, invalid position
Time Complexity: O(n) - May need to traverse up to n nodes
Space Complexity: O(1) - Only creating one new node
void insertAtPosition(int data, int position) {
if (position < 0) {
cout << "Invalid position!" << endl;
return;
}
if (position == 0) {
insertAtBeginning(data);
return;
}
Node* current = head;
// Traverse to position
for (int i = 0; i < position && current != nullptr; i++) {
current = current->next;
}
if (current == nullptr) { // Insert at end
insertAtEnd(data);
return;
}
// Insert before current
Node* newNode = new Node(data);
newNode->next = current;
newNode->prev = current->prev;
if (current->prev != nullptr) {
current->prev->next = newNode;
} else {
head = newNode; // New node becomes head
}
current->prev = newNode;
cout << "Inserted " << data << " at position " << position << endl;
}
d.) INSERT AFTER A GIVEN NODE
Theory: Insert a new node after a specific node (when node reference is available)
Approach:
1. Check if given node is valid
2. Create new node
3. Adjust four pointers carefully
4. Handle case when inserting after last node
Time Complexity: O(1) - Direct pointer manipulation, no traversal
Space Complexity: O(1) - Only creating one new node
This is very efficient when you already have the node reference!
void insertAfter(Node* prevNode, int data) {
if (prevNode == nullptr) {
cout << “ Given node is null!" << endl;
return;
}
Node* newNode = new Node(data);
newNode->next = prevNode->next;
newNode->prev = prevNode;
if (prevNode->next != nullptr) {
prevNode->next->prev = newNode;
}
prevNode->next = newNode;
cout << "Inserted " << data << " after given node" << endl;
}
e.) INSERT BEFORE A GIVEN NODE
Theory: Insert a new node before a specific node (when node reference is available)
Approach:
1. Check if given node is valid
2. Create new node
3. Adjust four pointers carefully
4. Handle case when inserting before head node
Time Complexity: O(1) - Direct pointer manipulation, no traversal
Space Complexity: O(1) - Only creating one new node
void insertBefore(Node* nextNode, int data) {
if (nextNode == nullptr) {
cout << " Given node is null!" << endl;
return;
}
Node* newNode = new Node(data);
newNode->next = nextNode;
newNode->prev = nextNode->prev;
if (nextNode->prev != nullptr) {
nextNode->prev->next = newNode;
} else {
head = newNode; // New node becomes head
}
nextNode->prev = newNode;
cout << "Inserted " << data << " before given node" << endl;
}
3.2 DELETION OPERATIONS
a.) DELETE FROM BEGINNING
Theory: Remove the first node and update head pointer
Approach:
1. Check if list is empty
2. Store head in temporary pointer
3. Move head to next node
4. If new head exists, set its prev to null
5. Delete the temporary node
Time Complexity: O(1) - Only updating few pointers
Space Complexity: O(1) - No extra space needed
Always constant time regardless of list size!
void deleteFromBeginning() {
if (head == nullptr) {
cout << "List is empty! Cannot delete." << endl;
return;
}
Node* temp = head;
head = head->next;
if (head != nullptr) {
head->prev = nullptr;
}
cout << "Deleted " << temp->data << " from beginning" << endl;
delete temp;
}
b.) DELETE FROM END
Theory: Remove the last node (without tail pointer)
Approach:
1. Check if list is empty
2. Handle single node case
3. Traverse to the last node
4. Update second-last node's next to null
5. Delete the last node
Time Complexity: O(n) - Need to traverse to end without tail pointer
Space Complexity: O(1) - No extra space needed
void deleteFromEnd() {
if (head == nullptr) {
cout << "List is empty! Cannot delete." << endl;
return;
}
// If only one node
if (head->next == nullptr) {
cout << "Deleted " << head->data << " from end (only node)" << endl;
delete head;
head = nullptr;
return;
}
// Traverse to the end
Node* current = head;
while (current->next != nullptr) {
current = current->next;
}
current->prev->next = nullptr;
cout << "Deleted " << current->data << " from end" << endl;
delete current;
}
c.) DELETE FROM POSITION
Theory: Remove node at a specific position (0-indexed)
Approach:
1. Handle edge cases: empty list, invalid position
2. Handle special case: position 0 (delete from beginning)
3. Traverse to target position
4. Adjust pointers of previous and next nodes
5. Delete the target node
Time Complexity: O(n) - May need to traverse up to n nodes
Space Complexity: O(1) - No extra space needed
void deleteFromPosition(int position) {
if (head == nullptr) {
cout << "List is empty! Cannot delete." << endl;
return;
}
if (position < 0) {
cout << "Invalid position!" << endl;
return;
}
if (position == 0) {
deleteFromBeginning();
return;
}
Node* current = head;
// Traverse to position
for (int i = 0; i < position && current != nullptr; i++) {
current = current->next;
}
if (current == nullptr) {
cout << "Position out of bounds!" << endl;
return;
}
// Adjust pointers
if (current->prev != nullptr) {
current->prev->next = current->next;
} else {
head = current->next; // Deleting head
}
if (current->next != nullptr) {
current->next->prev = current->prev;
}
cout << "Deleted " << current->data << " from position " << position << endl;
delete current;
}
d.) DELETE BY VALUE (First Occurrence)
Theory: Find and remove the first node with given value
Approach:
1. Traverse the list to find the target value
2. Once found, adjust the pointers of neighboring nodes
3. Handle special cases: deleting head, deleting last node
4. Delete the target node
Time Complexity: O(n) - May need to search entire list
Space Complexity: O(1) - No extra space needed
void deleteByValue(int value) {
if (head == nullptr) {
cout << "List is empty! Cannot delete." << endl;
return;
}
Node* current = head;
// Find the node
while (current != nullptr && current->data != value) {
current = current->next;
}
if (current == nullptr) {
cout << "Value " << value << " not found!" << endl;
return;
}
// Adjust pointers
if (current->prev != nullptr) {
current->prev->next = current->next;
} else {
head = current->next; // Deleting head
}
if (current->next != nullptr) {
current->next->prev = current->prev;
}
cout << "Deleted first occurrence of " << value << endl;
delete current;
}
e.) DELETE ALL OCCURRENCES
Theory: Remove all nodes with a specific value
Approach:
1. Traverse the entire list
2. For each node with target value:
- Adjust neighboring pointers
- Delete the node
3. Continue until end of list
Time Complexity: O(n) - Must visit every node once
Space Complexity: O(1) - No extra space needed
void deleteAllOccurrences(int value) {
Node* current = head;
int count = 0;
while (current != nullptr) {
Node* next = current->next;
if (current->data == value) {
if (current->prev != nullptr) {
current->prev->next = current->next;
} else {
head = current->next; // Deleting head
}
if (current->next != nullptr) {
current->next->prev = current->prev;
}
delete current;
count++;
}
current = next;
}
cout << "Deleted " << count << " occurrence(s) of " << value << endl;
}
f.) DELETE A SPECIFIC NODE
Theory: Delete a node when you have its reference (very efficient!)
Approach:
1. Check if node reference is valid
2. Adjust pointers of neighboring nodes
3. Handle special case: deleting head
4. Delete the node
Time Complexity: O(1) - Direct pointer manipulation
Space Complexity: O(1) - No extra space needed
void deleteNode(Node* nodeToDelete) {
if (nodeToDelete == nullptr) {
cout << "Node reference is null!" << endl;
return;
}
if (nodeToDelete->prev != nullptr) {
nodeToDelete->prev->next = nodeToDelete->next;
} else {
head = nodeToDelete->next; // Deleting head
}
if (nodeToDelete->next != nullptr) {
nodeToDelete->next->prev = nodeToDelete->prev;
}
cout << "Deleted node with value " << nodeToDelete->data << endl;
delete nodeToDelete;
}
TRAVERSAL OPERATIONS
a.) DISPLAY FORWARD
Theory: Print all elements from head to tail
Approach:
1. Start from head
2. Visit each node and print its data
3. Move to next node until null
Time Complexity: O(n) - Visit each node exactly once
Space Complexity: O(1) - No extra space (except for output)
void display() {
if (head == nullptr) {
cout << "List is empty!" << endl;
return;
}
Node* current = head;
cout << "Forward: ";
while (current != nullptr) {
cout << current->data << " <-> ";
current = current->next;
}
cout << "null" << endl;
}
b.) DISPLAY BACKWARD
Theory: Print all elements from tail to head (without tail pointer)
Approach:
1. First traverse to the end (O(n))
2. Then traverse backward using prev pointers (O(n))
3. Total: O(n) + O(n) = O(n)
Time Complexity: O(n) - Two passes through the list
Space Complexity: O(1) - No extra space needed
Note: With a tail pointer, this would be just O(n) with one pass!
void displayReverse() {
if (head == nullptr) {
cout << "List is empty!" << endl;
return;
}
// First, go to the end
Node* current = head;
while (current->next != nullptr) {
current = current->next;
}
// Then traverse backward
cout << "Backward: ";
while (current != nullptr) {
cout << current->data << " <-> ";
current = current->prev;
}
cout << "null" << endl;
}
c.) PRINT WITH POSITIONS
Theory: Display each element with its index position
Approach:
1. Traverse from head with position counter
2. Print both position and data for each node
Time Complexity: O(n) - Visit each node once
Space Complexity: O(1) - Only using position counter
void printWithPositions() {
Node* current = head;
int position = 0;
cout << "List with positions:" << endl;
while (current != nullptr) {
cout << "Position " << position << ": " << current->data << endl;
current = current->next;
position++;
}
}
SEARCH OPERATIONS
a.) SEARCH BY VALUE
Theory: Find the position of first occurrence of a value
Approach:
1. Start from head with position counter
2. Compare each node's data with target
3. Return position if found, -1 if not found
Time Complexity: O(n) - May need to check all nodes
Space Complexity: O(1) - Only using position counter
int search(int value) {
Node* current = head;
int position = 0;
while (current != nullptr) {
if (current->data == value) {
cout << "Found " << value << " at position " << position << endl;
return position;
}
current = current->next;
position++;
}
cout << "Value " << value << " not found!" << endl;
return -1;
}
b.) GET NODE AT POSITION
Theory: Return reference to node at specific position
Approach:
1. Validate position (non-negative)
2. Traverse to target position
3. Return node reference or null
Time Complexity: O(n) - May traverse up to n nodes
Space Complexity: O(1) - No extra space needed
Node* getNode(int position) {
if (position < 0) {
cout << "Invalid position!" << endl;
return nullptr;
}
Node* current = head;
for (int i = 0; i < position && current != nullptr; i++) {
current = current->next;
}
if (current == nullptr) {
cout << "Position " << position << " out of bounds!" << endl;
}
return current;
}
c.) FIND NODE WITH VALUE
Theory: Return reference to first node with specific value
Approach:
1. Traverse from head
2. Compare each node's data with target
3. Return node reference if found, null otherwise
Time Complexity: O(n) - May search entire list
Space Complexity: O(1) - No extra space needed
Node* findNode(int value) {
Node* current = head;
while (current != nullptr) {
if (current->data == value) {
return current;
}
current = current->next;
}
return nullptr;
}
d.) CHECK IF VALUE EXISTS
Theory: Boolean check for value existence
Approach:
1. Use search function internally
2. Return true if position >= 0, false otherwise
Time Complexity: O(n) - Uses search internally
Space Complexity: O(1) - No extra space needed
bool contains(int value) {
return search(value) != -1;
}
e.) COUNT OCCURRENCES
Theory: Count how many times a value appears in the list
Approach:
1. Traverse entire list
2. Increment counter for each matching value
3. Return total count
Time Complexity: O(n) - Must check every node
Space Complexity: O(1) - Only using counter variable
int countOccurrences(int value) {
Node* current = head;
int count = 0;
while (current != nullptr) {
if (current->data == value) {
count++;
}
current = current->next;
}
cout << "Value " << value << " appears " << count << " time(s)" << endl;
return count;
}
UPDATE OPERATIONS
a.) UPDATE AT POSITION
Theory: Change value of node at specific position
Approach:
1. Use getNode() to find target position
2. If node exists, update its data
3. Return success/failure status
Time Complexity: O(n) - Uses getNode() which is O(n)
Space Complexity: O(1) - No extra space needed
bool updateAtPosition(int position, int newValue) {
Node* node = getNode(position);
if (node != nullptr) {
int oldValue = node->data;
node->data = newValue;
cout << "Updated position " << position << " from " << oldValue << " to " <<
newValue << endl;
return true;
}
return false;
}
b.) UPDATE BY VALUE (First Occurrence)
Theory: Update first node that matches given value
Approach:
1. Traverse list to find first matching value
2. Update that node's data
3. Return success/failure status
Time Complexity: O(n) - May search entire list
Space Complexity: O(1) - No extra space needed
bool updateByValue(int oldValue, int newValue) {
Node* current = head;
while (current != nullptr) {
if (current->data == oldValue) {
current->data = newValue;
cout << "Updated first occurrence of " << oldValue << " to " << newValue <<
endl;
return true;
}
current = current->next;
}
cout << "Value " << oldValue << " not found!" << endl;
return false;
}
c.) UPDATE ALL OCCURRENCES
Theory: Update all nodes that match given value
Approach:
1. Traverse entire list
2. Update every node with matching value
3. Return count of updates made
Time Complexity: O(n) - Must check every node
Space Complexity: O(1) - Only using counter variable
int updateAllOccurrences(int oldValue, int newValue) {
Node* current = head;
int count = 0;
while (current != nullptr) {
if (current->data == oldValue) {
current->data = newValue;
count++;
}
current = current->next;
}
if (count > 0) {
cout << "Updated " << count << " occurrence(s) of " << oldValue << " to " <<
newValue << endl;
} else {
cout << "Value " << oldValue << " not found!" << endl;
}
return count;
}
UTILITY OPERATIONS
a.) GET SIZE
Theory: Count total number of nodes in the list
Approach:
1. Traverse entire list with counter
2. Return final count
Time Complexity: O(n) - Must visit every node
Space Complexity: O(1) - Only using counter variable
Note: If you frequently need size, consider maintaining a size variable!
int size() {
int count = 0;
Node* current = head;
while (current != nullptr) {
count++;
current = current->next;
}
return count;
}
b.) IS EMPTY CHECK
Theory: Check if list has any nodes
Approach:
1. Simply check if head is null
Time Complexity: O(1) - Single pointer check
Space Complexity: O(1) - No extra space needed
bool isEmpty() {
return head == nullptr;
}
c.) GET FRONT ELEMENT
Theory: Return data of first node
Approach:
1. Check if list is empty
2. Return head's data
Time Complexity: O(1) - Direct access to head
Space Complexity: O(1) - No extra space needed
int front() {
if (head != nullptr) {
return head->data;
}
throw runtime_error("List is empty!");
}
d.) GET BACK ELEMENT
Theory: Return data of last node
Approach:
1. Check if list is empty
2. Traverse to last node
3. Return last node's data
Time Complexity: O(n) - Need to traverse to end
Space Complexity: O(1) - No extra space needed
int back() {
if (head == nullptr) {
throw runtime_error("List is empty!");
}
Node* current = head;
while (current->next != nullptr) {
current = current->next;
}
return current->data;
}
e.) CLEAR LIST
Theory: Delete all nodes and reset list to empty state
Approach:
1. Traverse list and delete each node
2. Set head to null
Time Complexity: O(n) - Must delete every node
Space Complexity: O(1) - No extra space needed
void clear() {
while (head != nullptr) {
Node* temp = head;
head = head->next;
delete temp;
}
cout << "List cleared!" << endl;
}
ADVANCED OPERATIONS (CP Specific)
1.) REVERSE LIST
Theory: Reverse the direction of all links in the list
Approach:
1. Traverse the list
2. For each node, swap its next and prev pointers
3. Update head to point to original tail
Time Complexity: O(n) - Visit each node once
Space Complexity: O(1) - Only using temporary pointers
void reverse() {
Node* current = head;
Node* temp = nullptr;
// Swap next and prev for all nodes
while (current != nullptr) {
temp = current->prev;
current->prev = current->next;
current->next = temp;
current = current->prev; // Move to next node (which is now prev)
}
// Update head
if (temp != nullptr) {
head = temp->prev;
}
cout << "List reversed!" << endl;
}
2.) FIND MIDDLE ELEMENT
Theory: Find middle element using Floyd's Cycle Detection (Slow-Fast Pointer)
Approach:
1. Use two pointers: slow (moves 1 step) and fast (moves 2 steps)
2. When fast reaches end, slow will be at middle
3. For even length, returns first middle element
// Find middle element (for even length, returns first middle) - O(n)
int findMiddle() {
if (head == nullptr) {
throw runtime_error("List is empty");
}
Node* slow = head;
Node* fast = head;
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
}
return slow->data;
}
● Time Complexity: O(n)
● Space Complexity: O(1)
3.) CHECK PALINDROME
Theory: Compare lists from both ends using two pointers.
Approach:
1. Traverse to end to get tail.
2. Compare head and tail moving inward.
3. If a mismatch is found, return false.
bool isPalindrome() {
if (head == nullptr) return true;
// Find the end
Node* end = head;
while (end->next != nullptr) {
end = end->next;
}
// Compare from both ends
Node* start = head;
while (start != end && start->prev != end) {
if (start->data != end->data) {
return false;
}
start = start->next;
end = end->prev;
}
return true;
}
● Time Complexity: O(n)
● Space Complexity: O(1)
4.) REMOVE DUPLICATES (SORTED LIST)
Theory: Since the list is sorted, duplicates are adjacent.
Approach:
1. Traverse from head.
2. If current->data == current->next->data, remove current->next.
void removeDuplicatesFromSorted() {
Node* current = head;
while (current != nullptr && current->next != nullptr) {
if (current->data == current->next->data) {
Node* duplicate = current->next;
current->next = duplicate->next;
if (duplicate->next != nullptr) {
duplicate->next->prev = current;
}
delete duplicate;
} else {
current = current->next;
}
}
}
● Time Complexity: O(n)
● Space Complexity: O(1)
5.) REMOVE DUPLICATES (UNSORTED LIST)
Theory: Use two nested loops to compare each node with the rest.
Approach:
1. For each node, check all next nodes for duplicates.
2. If a duplicate is found, adjust pointers and delete it.
void removeDuplicatesFromUnsorted() {
Node* current = head;
while (current != nullptr) {
Node* runner = current->next;
while (runner != nullptr) {
if (runner->data == current->data) {
Node* duplicate = runner;
runner = runner->next;
if (duplicate->prev != nullptr) {
duplicate->prev->next = duplicate->next;
}
if (duplicate->next != nullptr) {
duplicate->next->prev = duplicate->prev;
}
delete duplicate;
} else {
runner = runner->next;
}
}
current = current->next;
}
}
● Time Complexity: O(n²)
● Space Complexity: O(1)
6.) ROTATE LIST RIGHT BY k POSITIONS
Theory: Move the last k nodes to the front.
Approach:
1. Calculate length and take k % len.
2. Find a new tail: len - k - 1 steps from head.
3. Re-link nodes accordingly.
// Rotate list k positions to the right - O(n)
void rotateRight(int k) {
if (head == nullptr || k == 0) return;
int len = size();
k = k % len; // Handle k > len
if (k == 0) return;
// Find the new tail (len - k - 1 from start)
Node* newTail = head;
for (int i = 0; i < len - k - 1; i++) {
newTail = newTail->next;
}
// Find the current tail
Node* currentTail = head;
while (currentTail->next != nullptr) {
currentTail = currentTail->next;
}
// Perform rotation
Node* newHead = newTail->next;
newTail->next = nullptr;
newHead->prev = nullptr;
currentTail->next = head;
head->prev = currentTail;
head = newHead;
}
● Time Complexity: O(n)
● Space Complexity: O(1)
7.) CREATE FROM ARRAY
Theory: Build the list from a given array.
Approach:
● Insert elements at the end one by one.
// Create from array - O(n)
void createFromArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
insertAtEnd(arr[i]);
}
}
● Time Complexity: O(n)
● Space Complexity: O(1)
8.) CONVERT TO ARRAY
Theory: Store list elements into a vector.
Approach:
● Traverse list and push each data to vector.
// Convert to array - O(n)
vector<int> toArray() {
vector<int> result;
Node* current = head;
while (current != nullptr) {
result.push_back(current->data);
current = current->next;
}
return result;
}
9.) BUBBLE SORT (DATA SWAP)
Theory: Compare adjacent elements and swap if unordered.
Approach:
● Repeat passes until no swaps.
● Inner loop compares current with current->next.
void bubbleSort() {
if (head == nullptr) return;
bool swapped;
Node* current;
Node* last = nullptr;
do {
swapped = false;
current = head;
while (current->next != last) {
if (current->data > current->next->data) {
// Swap data
int temp = current->data;
current->data = current->next->data;
current->next->data = temp;
swapped = true;
}
current = current->next;
}
last = current;
} while (swapped);
}
};
int main() {
DoublyLinkedList list;
// Create initial list
int arr[] = {10, 20, 30, 40, 50};
list.createFromArray(arr, 5);
list.display();
// Test insertions
list.insertAtBeginning(5);
cout << "After inserting 5 at beginning: ";
list.display();
list.insertAtEnd(60);
cout << "After inserting 60 at end: ";
list.display();
list.insertAtPosition(25, 4);
cout << "After inserting 25 at position 4: ";
list.display();
// Test traversals
list.displayReverse();
// Test searches
cout << "Position of 30: " << list.search(30) << endl;
cout << "Contains 25: " << (list.contains(25) ? "Yes" : "No") << endl;
cout << "Count of 20: " << list.countOccurrences(20) << endl;
// Test updates
list.updateAtPosition(2, 35);
cout << "After updating position 2 to 35: ";
list.display();
// Test deletions
list.deleteFromBeginning();
cout << "After deleting from beginning: ";
list.display();
list.deleteFromEnd();
cout << "After deleting from end: ";
list.display();
list.deleteByValue(25);
cout << "After deleting value 25: ";
list.display();
// Test advanced operations
cout << "List size: " << list.size() << endl;
cout << "Front element: " << list.front() << endl;
cout << "Back element: " << list.back() << endl;
cout << "Middle element: " << list.findMiddle() << endl;
// Test palindrome
DoublyLinkedList palindromeList;
int palArr[] = {1, 2, 3, 2, 1};
palindromeList.createFromArray(palArr, 5);
cout << "Palindrome list [1,2,3,2,1]: ";
palindromeList.display();
cout << "Is palindrome: " << (palindromeList.isPalindrome() ? "Yes" : "No") << endl;
// Test reverse
cout << "Before reverse: ";
list.display();
list.reverse();
cout << "After reverse: ";
list.display();
// Test sorting
DoublyLinkedList unsortedList;
int unsortedArr[] = {64, 34, 25, 12, 22, 11, 90};
unsortedList.createFromArray(unsortedArr, 7);
cout << "Before sorting: ";
unsortedList.display();
unsortedList.bubbleSort();
cout << "After bubble sort: ";
unsortedList.display();
return 0;