In the official GESP C++ Level 5 exam outline, there are a total of <span>9</span> key points. This article analyzes and introduces the <span>3</span> key point.
(3) Master the creation, insertion, deletion, traversal, and reversal operations of linked lists, and understand the differences between singly linked lists, doubly linked lists, and circular linked lists.
Due to the extensive content and the involvement of code writing and verification, this knowledge point will be introduced in three parts: singly linked lists, doubly linked lists, and circular linked lists.
I am also learning, experimenting, and summarizing, and my grasp of the depth and breadth of the exam outline is based on personal understanding. Therefore, this article is more of a personal knowledge review rather than a tutorial. Any omissions or oversights are welcome for correction and discussion.
Review of other Level 5 key points:
- 【GESP】C++ Level 5 Exam Outline Knowledge Points Review, (1) Elementary Number Theory
- 【GESP】C++ Level 5 Exam Outline Knowledge Points Review, (2) Simulating High-Precision Calculations
- 【GESP】C++ Level 5 Exam Outline Knowledge Points Review, (3-1) Linked Lists – Singly Linked Lists
1. Basic Concepts of Doubly Linked Lists
A doubly linked list (Doubly Linked List) is a type of linked data structure where each node contains three parts:
- Data Field (data)
- Previous Pointer (prev): points to the previous node
- Next Pointer (next): points to the next node
Compared to singly linked lists, the advantage of doubly linked lists is that they allow for bidirectional traversal, and insertion and deletion are more efficient when the node pointer is known; the disadvantage is that it requires additional space to store the <span>prev</span> pointer.
1.1 Characteristics of Doubly Linked Lists
| Characteristic | Doubly Linked List |
|---|---|
| Pointer Direction | Each node has <span>prev</span> and <span>next</span> pointers |
| Advantages | Supports forward and backward traversal; insertion/deletion can be done when the node pointer is known |
| Disadvantages | Uses more memory; implementation is slightly more complex |
2. Basic Operations of Doubly Linked Lists
2.1 Node Definition
// Define the structure of a doubly linked list node
struct DNode {
int data; // Data field
DNode* prev; // Points to the previous node
DNode* next; // Points to the next node
// Constructor
DNode(int val) : data(val), prev(nullptr), next(nullptr) {}
};
2.2 Creating a Doubly Linked List
Creating a doubly linked list mainly involves the following steps:
-
Initialize Pointers
- Create head pointer
<span>head</span>and tail pointer<span>tail</span>, both initially null - Used to track the positions of the head and tail nodes of the list
Create New Nodes
- Create new nodes for each data value
- Set the data field and pointer fields of the nodes
Establish Connections Between Nodes
- Set the
<span>prev</span>pointer of the new node to point to the current tail node - Set the
<span>next</span>pointer of the current tail node to point to the new node - Update the tail pointer to the new node
- Point both head and tail pointers to this node
- If it is the first node:
- If it is not the first node:
Return the Head of the List
- Return the head pointer of the created list
// Create a doubly linked list
// @param vals: Array of data used to create the list
// @return: Returns the head pointer of the created doubly linked list
DNode* createDList(const vector<int>& vals) {
DNode* head = nullptr; // Head pointer, points to the first node of the list
DNode* tail = nullptr; // Tail pointer, points to the last node of the list
// Traverse the input array and create nodes one by one
for (int val : vals) {
DNode* newNode = new DNode(val); // Create a new node
if (!head) {
// If it is the first node, update head and tail pointers
head = tail = newNode;
} else {
// Otherwise, append the new node at the tail
tail->next = newNode; // Current tail node's next points to the new node
newNode->prev = tail; // New node's prev points to the old tail node
tail = newNode; // Update tail pointer to the new node
}
}
// Return the head pointer of the list
return head;
}
2.3 Inserting Nodes
2.3.1 Inserting at a Given Position
Inserting a node at a given position mainly involves the following steps:
-
Check Position Validity
- If the position is 0, insert at the head
- If the position is greater than the length of the list, insert at the tail
- Otherwise, traverse to the node before the specified position
Create New Node
- Create a new linked list node for the new value
Insert Node
- Insert the new node at the target position and update pointers
- If inserting at the head, update the head pointer
- If inserting in the middle, update the
<span>next</span>pointer of the previous node and the<span>prev</span>pointer of the new node - If inserting at the tail, update the tail pointer and the
<span>next</span>pointer of the previous node
// Insert a node at position pos (0-based)
// @param head: Reference to the head pointer of the doubly linked list, to modify the head pointer
// @param pos: The position to insert (0 means head)
// @param val: The value of the new node to insert
void insertAtPosition(DNode*& head, int pos, int val) {
DNode* newNode = new DNode(val); // Create a new node
// If inserting at the head
if (pos == 0) {
newNode->next = head; // New node's next points to the original head node
if (head) {
head->prev = newNode; // If the original head node exists, prev points to the new node
}
head = newNode; // Update head pointer to the new node
return;
}
DNode* cur = head; // Start traversing from the head node
// Traverse to the node before the insertion position
for (int i = 0; i < pos - 1 && cur; ++i) {
cur = cur->next;
}
// If the previous node for the insertion position is found
if (cur) {
newNode->next = cur->next; // New node's next points to the next node at the current position
newNode->prev = cur; // New node's prev points to the current node
if (cur->next) // If the next node at the current position exists
cur->next->prev = newNode; // The next node's prev points to the new node
cur->next = newNode; // Current node's next points to the new node
}
}
2.3.2 Inserting After a Given Node
Inserting a new node after a given node mainly involves the following steps:
-
Check if the Target Node is Null
- If the target node is null, return directly without performing the insertion operation
Create New Node
- Create a new linked list node for the new value
Insert Node
- Insert the new node at the target position and update pointers
- The new node’s
<span>next</span>points to the next node of the target node - The new node’s
<span>prev</span>points to the target node - If the next node of the target node exists, update its
<span>prev</span>pointer to point to the new node - The target node’s
<span>next</span>points to the new node
// Insert a new node after the specified target node
// @param target: Pointer to the target node, where the new node will be inserted after
// @param val: The value of the new node to insert
void insertAfter(DNode* target, int val) {
if (!target) {
return; // If the target node is null, return directly
}
DNode* newNode = new DNode(val); // Create a new node
// New node's next points to the target node's next node
newNode->next = target->next;
// New node's prev points to the target node
newNode->prev = target;
// If the target node's next node exists, let its prev point to the new node
if (target->next)
target->next->prev = newNode;
// Target node's next points to the new node
target->next = newNode;
}
2.4 Deleting Nodes
2.4.1 Deleting at a Specified Position
Deleting a node at a specified position mainly involves the following steps:
-
Check if the List is Empty
- If the list is empty, return directly without performing the deletion operation
Check the Position to Delete
- If the position to delete is 0, delete the head node
- Otherwise, traverse to the node before the specified position
Delete Node
- If the previous node for the position to delete exists, update its
<span>next</span>pointer to point to the next node of the node to be deleted - If the next node of the node to be deleted exists, update its
<span>prev</span>pointer to point to the previous node of the node to be deleted - Free the memory of the node to be deleted
// Delete the node at the specified position
// @param head: Reference to the head pointer of the doubly linked list
// @param pos: The position of the node to delete (0-based)
void deleteAtPosition(DNode*& head, int pos) {
if (!head) {
// If the list is empty, return directly
return;
}
DNode* cur = head; // Pointer for traversal
// If the node to delete is the head node
if (pos == 0) {
head = head->next; // Head pointer points to the next node
if (head) {
head->prev = nullptr; // New head node's prev set to nullptr
}
delete cur; // Free the original head node
return;
}
// Traverse to the node to delete
for (int i = 0; i < pos && cur; ++i) {
cur = cur->next;
}
// If the node to delete is found
if (cur) {
if (cur->prev) {
cur->prev->next = cur->next; // Previous node's next points to the next node of the current node
}
if (cur->next) {
cur->next->prev = cur->prev; // Next node's prev points to the previous node of the current node
}
delete cur; // Free the current node
}
}
2.4.2 Deleting a Node with a Specified Value
Deleting the first node with the value val in the list mainly involves the following steps:
-
Check if the List is Empty
- If the list is empty, return directly without performing the deletion operation
Traverse the List to Find the Node
- Start traversing the list from the head node to find the first node with the value val
Delete Node
- If the node is found, update the next pointer of the previous node to point to the next node of the current node
- If the node to delete is the head node, update the head pointer to the next node
- If the current node is not the tail node, update the prev pointer of the next node to point to the previous node of the current node
- Free the memory of the current node
// Delete the first node with the value val in the list
// @param head: Reference to the head pointer of the doubly linked list, to modify the head pointer
// @param val: The value of the node to delete (only deletes the first matching node)
void deleteNode(DNode*& head, int val) {
DNode* cur = head; // Pointer for traversing the list
// Traverse the list to find the first node with the value val
while (cur && cur->data != val) {
cur = cur->next;
}
// If the node is found
if (cur) {
// If the node to delete is not the head node
if (cur->prev) {
// Previous node's next points to the current node's next node
cur->prev->next = cur->next;
} else {
// If the node to delete is the head node, update the head pointer
head = cur->next;
}
// If the current node is not the tail node
if (cur->next) {
// Next node's prev points to the current node's previous node
cur->next->prev = cur->prev;
}
// Free the current node's memory
delete cur;
}
}
2.5 Traversing the List
2.5.1 Forward Traversal
Forward traversing the list mainly involves the following steps:
-
Check if the List is Empty
- If the list is empty, return directly without performing the traversal operation
Traverse the List
- Start traversing the list from the head node, moving to the next node each time
- Output the data of the current node
- Continue until reaching the tail node, ending the traversal
// Forward traverse the doubly linked list
void traverseForward(DNode* head) {
// Start traversing from the head node
// Continue traversing while cur is not null, moving to the next node each time
for (DNode* cur = head; cur; cur = cur->next)
// Output the data of the current node
cout << cur->data << " ";
// New line
cout << endl;
}
2.5.2 Backward Traversal
Backward traversing the list mainly involves the following steps:
-
Check if the List is Empty
- If the list is empty, return directly without performing the traversal operation
Traverse the List
- Start traversing the list from the tail node, moving to the previous node each time
- Output the data of the current node
- Continue until reaching the head node, ending the traversal
// Backward traverse the doubly linked list
void traverseBackward(DNode* tail) {
// Start traversing from the tail node
// Continue traversing while cur is not null, moving to the previous node each time
for (DNode* cur = tail; cur; cur = cur->prev)
// Output the data of the current node
cout << cur->data << " ";
// New line
cout << endl;
}
2.6 Reversing the List
Reversing the list mainly involves the following steps:
-
Check if the List is Empty
- If the list is empty, return directly without performing the reversal operation
Reverse the List
- Start traversing the list from the head node, moving to the next node each time
- Swap the
<span>prev</span>and<span>next</span>pointers of the current node - Continue until reaching the tail node, ending the traversal
- Finally, return the new head node
// Reverse the doubly linked list
// @param head: Head pointer of the doubly linked list
// @return: Head pointer of the reversed list
DNode* reverseDList(DNode* head) {
DNode* cur = head; // Current node being traversed
DNode* tmp = nullptr; // Temporary variable to hold the previous pointer
// Traverse the list, swapping each node's prev and next pointers
while (cur) {
// First save the current node's prev pointer (original predecessor of the list)
tmp = cur->prev;
// Swap the current node's prev and next pointers
cur->prev = cur->next;
cur->next = tmp;
// Move to the next node (since prev and next have been swapped, use cur->prev)
cur = cur->prev;
}
// During the reversal, each node's prev and next have been swapped.
// When the loop ends, cur is null, and tmp is the previous node of the tail node (original second to last node)
// Since prev and next have been swapped, tmp->prev actually points to the last node of the original list (i.e., the new head node).
// Therefore, we need to assign head to tmp->prev to get the new head node after reversal.
if (tmp) {
head = tmp->prev;
}
return head;
}
3. Summary
- The insertion/deletion of doubly linked lists can be completed when the node pointer is known, while searching remains .
- Maintaining an additional
<span>prev</span>pointer increases space usage, but provides stronger functionality. - Backward traversal, deleting the tail node, and other operations are more convenient than singly linked lists.
4. Complete Test Code
// Example usage
int main() {
// Create a vector containing integers from 1 to 5
vector<int> vals = {1, 2, 3, 4, 5};
// Create a doubly linked list from the vector
DNode* head = createDList(vals);
// Forward traverse the list and output
traverseForward(head);
// Backward traverse the list and output (starting from the last node)
traverseBackward(head->next->next->next->next);
// Insert a node with value 6 at position 2
insertAtPosition(head, 2, 6);
// Forward traverse the list and output
traverseForward(head);
// Insert a node with value 0 at the head
insertAtPosition(head, 0, 0);
// Forward traverse the list and output
traverseForward(head);
// Insert a node with value 7 after the 3rd node (value 3)
insertAfter(head->next->next, 7);
// Forward traverse the list and output
traverseForward(head);
// Delete the head node
deleteAtPosition(head, 0);
// Forward traverse the list and output
traverseForward(head);
// Delete the middle node (originally the 3rd node, now the 2nd node, value 3)
deleteAtPosition(head, 2);
// Forward traverse the list and output
traverseForward(head);
// Delete the node with value 7
deleteNode(head, 7);
// Forward traverse the list and output
traverseForward(head);
// Delete the node with value 6
deleteNode(head, 6);
// Forward traverse the list and output
traverseForward(head);
// Delete the node with value 1
deleteNode(head, 1);
// Forward traverse the list and output
traverseForward(head);
// Delete the node with value 0
deleteNode(head, 0);
// Forward traverse the list and output
traverseForward(head);
// Delete the node with value 5
deleteNode(head, 5);
// Forward traverse the list and output
traverseForward(head);
// Insert a node with value 1 at the head
insertAtPosition(head, 0, 1);
// Forward traverse the list and output
traverseForward(head);
// Insert a node with value 5 at position 4
insertAtPosition(head, 4, 5);
// Forward traverse the list and output
traverseForward(head);
// Reverse the list
head = reverseDList(head);
// Forward traverse the list and output
traverseForward(head);
// End of program
return 0;
}
Example Output:
1 2 3 4 5
5 4 3 2 1
1 2 6 3 4 5
0 1 2 6 3 4 5
0 1 2 7 6 3 4 5
1 2 7 6 3 4 5
1 2 6 3 4 5
1 2 6 3 4 5
1 2 3 4 5
2 3 4 5
2 3 4 5
2 3 4
1 2 3 4
1 2 3 4 5
5 4 3 2 1
Please open in the WeChat client
For GESP exam outlines, real questions explanations, knowledge expansion, and practice lists, see:
[Top] [GESP] C++ Certification Learning Resource Summary
“luogu-” series problems can be evaluated online in the Luogu Problem Bank.
“bcqm-” series problems can be evaluated online in the Programming Enlightenment Problem Bank.