-UNIT 3- Linked List -1
-UNIT 3- Linked List -1
-UNIT 3- Linked List -1
Concept of linked organization, singly and doubly linked list and dynamic storage management,
circular linked list, operations such as insertion, deletion, concatenation, traversal of linked list,
dynamic memory management, garbage collection.
Linked Organization is a type of data organization where elements (also known as nodes) are
not stored contiguously in memory but are linked to each other using pointers. Each element of
a linked structure contains the data and a reference (pointer) to the next element, making it
possible to traverse the data even if it is scattered throughout the memory.
Key Concepts:
1. Node: A node is the fundamental building block of a linked organization. A node typically
consists of two parts:
Pointer part: This contains the address (or reference) to the next node in the sequence.
Singly Linked List (SLL): Each node contains a single pointer that points to the next node. The
last node in the list points to null (or None), indicating the end of the list.
Doubly Linked List (DLL): Each node contains two pointers: one pointing to the next node and
another pointing to the previous node, allowing traversal in both directions.
null <- [prev | data | next] <-> [prev | data | next] <-> ... -> null
Circular Linked List (CLL): The last node in a circular linked list points back to the first node,
creating a loop in the structure. This can be implemented as a singly or doubly linked list.
3. Key Operations:
Updating the pointer of the new node to link it correctly. Insertion can occur at the beginning,
middle, or end of the list.
Removing the node by updating the pointers of adjacent nodes to bypass the node to be
deleted.
Traversal: Traversal is the process of visiting each node in the list. For a singly linked list,
traversal starts from the head (first node) and moves sequentially to the next nodes until
reaching null. In doubly and circular linked lists, traversal can be done in both directions.
Dynamic Size: Unlike arrays, linked lists can grow and shrink dynamically, allowing efficient
memory utilization.
Flexible Memory Allocation: Linked lists do not require contiguous memory allocation, making
them more adaptable in environments with limited memory.
Random Access Not Possible: Linked lists do not allow direct access to elements based on
index; elements can only be accessed sequentially.
Complexity in Implementation: Linked structures are generally more complex to implement than
arrays because of the need to manage pointers carefully.
1. Implementation of Stacks and Queues: Linked lists provide efficient implementations for
stacks and queues by allowing dynamic growth and shrinkage without the need for resizing (as
in arrays).
2. Representation of Sparse Matrices: Linked lists are used to represent sparse matrices where
most of the elements are zero, avoiding the need to store all elements.
3. Graph Representation: Linked structures like adjacency lists are commonly used to represent
graphs efficiently, especially when the graph is sparse.
4. Dynamic Memory Allocation: Memory management schemes like heap memory often use
linked structures to track free memory blocks.
5. Polynomial Representation: Linked lists are used to represent and manipulate polynomials
where each node stores a term of the polynomial.
A Singly Linked List (SLL) is a linear data structure where each node points to the next node in
the sequence, and the last node points to null. It consists of a sequence of nodes, with each
node containing two parts:
Key Properties:
1. Head Node: The first node in the list, often referred to as the head, is the entry point to the
list.
2. Tail Node: The last node in the list, which points to null, signifying the end of the list.
3. Unidirectional Traversal: Traversal is only possible in one direction, from the head to the last
node.
1. Insertion:
At the Beginning: A new node is inserted before the head, and the head pointer is updated to
this new node.
At the End: The new node is inserted after the current tail node, and the next pointer of the tail
node is updated to the new node.
At a Specific Position: Inserting at a given position requires traversing to that position and
updating the pointers of adjacent nodes.
2. Deletion:
From the Beginning: The head node is removed by making the second node the new head.
From the End: Deletion from the end requires traversing to the second-to-last node and
updating its next pointer to null.
From a Specific Position: Similar to insertion, deletion requires traversal to the node before the
target node, updating its pointer to bypass the target node.
3. Traversal:
Traversing through the list involves starting from the head node and following the next pointers
until the null is reached.
Advantages of Singly Linked List:
Dynamic Size: Can grow or shrink in size based on the number of elements.
Efficient Insertions and Deletions: Insertion and deletion operations (at the head or tail) are
efficient, requiring minimal changes to pointers.
No Backward Traversal: It is not possible to traverse backward since each node only has a
pointer to the next node.
More Memory: Requires extra memory for storing the pointer to the next node.
A Doubly Linked List (DLL) is a type of linked list in which each node contains two pointers:
Next Pointer: Contains the address of the next node in the sequence.
Previous Pointer: Contains the address of the previous node, allowing traversal in both
directions.
Key Properties:
1. Head Node: The first node in the list, which has its previous pointer set to null.
2. Tail Node: The last node in the list, which has its next pointer set to null.
3. Bidirectional Traversal: Nodes can be traversed in both forward and backward directions.
1. Insertion:
At the Beginning: A new node is inserted before the head, with its next pointer pointing to the
current head, and the head’s previous pointer updated to this new node.
At the End: A new node is inserted after the current tail node, and both the previous and next
pointers are updated accordingly.
At a Specific Position: Insertion at a given position involves updating both the next and previous
pointers of adjacent nodes.
2. Deletion:
From the Beginning: The head node is removed by updating the head to the next node and
setting the new head's previous pointer to null.
From the End: The tail node is removed by updating the previous node's next pointer to null.
From a Specific Position: Similar to insertion, deletion involves adjusting both next and previous
pointers to bypass the node to be deleted.
3. Traversal:
Forward Traversal: Starts from the head and moves in the forward direction using the next
pointer.
Backward Traversal: Starts from the tail and moves in the backward direction using the previous
pointer.
Bidirectional Traversal: Allows traversal in both forward and backward directions, which is more
flexible than singly linked lists.
Efficient Deletion: Deleting a node in the middle is more efficient as we can traverse the list in
either direction to reach the node faster.
More Memory Overhead: Each node requires additional memory for storing the previous pointer.
Complexity in Implementation: Operations such as insertion and deletion require more pointer
adjustments compared to singly linked lists.
A Singly Linked List (SLL) is a type of linked list where each node has two components:
Pointer (or link) part: Stores the address of the next node.
The last node’s pointer is set to null, indicating the end of the list.
struct Node {
int data;
struct Node* next;
// Pointer to the next node
};
At the beginning:
Algorithm:
At the end:
Create a new node and link the last node's next pointer to the new node.
Algorithm:
At a given position:
Traverse the list to the desired position.
Insert the new node by adjusting the next pointers of adjacent nodes.
Algorithm:
At the beginning:
The head is updated to point to the second node, and the first node is deleted.
Algorithm:
def delete_at_beginning(head):
if head is None:
return None # List is empty
head = head.next
return head # Return new head
At the end:
Set its next pointer to null and delete the last node.
Algorithm:
def delete_at_end(head):
if head is None:
return None # List is empty
if head.next is None:
return None # Only one element
current = head
while current.next.next is not None: # Traverse to second-last
node
current = current.next
current.next = None # Remove last node
return head
At a given position:
Traverse to the node just before the node to be deleted, and adjust pointers accordingly.
Algorithm:
Algorithm:
def traverse(head):
current = head
while current is not None:
print(current.data)
current = current.next
A Doubly Linked List (DLL) is a type of linked list where each node contains:
Pointer to the next node: A reference to the next node in the list.
Pointer to the previous node: A reference to the previous node, allowing bi-directional traversal.
struct Node {
int data;
struct Node* prev; // Pointer to the previous node
struct Node* next; // Pointer to the next node
};
At the beginning:
Adjust the next pointer of the new node to point to the current head.
Adjust the prev pointer of the current head to point to the new node.
Algorithm:
At the end:
Adjust pointers of the last node and the new node accordingly.
Algorithm:
At a given position:
Similar to the singly linked list, but adjust both the next and prev pointers of adjacent nodes.
Algorithm:
At the beginning:
Algorithm:
def delete_at_beginning(head):
if head is None:
return None
head = head.next
if head is not None:
head.prev = None
return head
At the end:
Algorithm:
def delete_at_end(head):
if head is None:
return None
if head.next is None:
return None # Only one element
last = head
while last.next is not None:
last = last.next
last.prev.next = None
return head
At a given position:
Adjust both next and prev pointers of adjacent nodes to bypass the node being deleted.
Algorithm:
For backward traversal, start from the last node and move through the prev pointers.
Algorithm:
def traverse_forward(head):
current = head
while current is not None:
print(current.data)
current = current.next
def traverse_backward(head):
current = head
while current.next is not None:
A circular linked list is a variation of a linked list where the last node points back to the head
node instead of pointing to NULL. This forms a circular structure, allowing traversal from any
node to any other node in the list.
In Circular Singly Linked List, each node has just one pointer called the “next” pointer. The next
pointer of last node points back to the first node and this results in forming a circle. In this type
of Linked list we can only move through the list in one direction.
In circular doubly linked list, each node has two pointers prev and next, similar to doubly linked
list. The prev pointer points to the previous node and the next points to the next node. Here, in
addition to the last node storing the address of the first node, the first node will also store the
address of the last node.
1. Insertion
At the beginning: Insert a new node as the head, and update the last node's next to point to the
new head.
At the end: Insert a new node after the last node and update its next to point to the head.
At a specific position: Traverse to the required position and adjust the next pointers.
2. Deletion
From the beginning: Remove the head node and update the last node's next to point to the new
head.
From the end: Traverse to the second-last node and update its next to point to the head.
From a specific position: Traverse to the required position, adjust the next pointer of the
preceding node to skip the target node.
3. Traversal
Start at any node (commonly the head) and move to the next node until you reach the starting
node again.
Algorithms
Algorithm: deleteFromEnd(head)
1. If head is NULL:
a. Print "List is empty."
b. Exit.
2. Else if head->next == head:
a. Free head.
b. Set head = NULL.
3. Else:
a. Set temp = head and prev = NULL.
b. While temp->next != head:
i. Set prev = temp.
ii. Move temp = temp->next.
c. Set prev->next = head.
d. Free temp.
4. End.
Algorithm: traverse(head)
1. If head is NULL:
a. Print "List is empty."
b. Exit.
2. Else:
a. Set temp = head.
b. Do:
i. Print temp->data.
ii. Move temp = temp->next.
While temp != head.
3. End.
Advantages
Disadvantages
Dynamic memory management in data structures refers to the process of allocating and
deallocating memory at runtime. Unlike static memory allocation, where the memory size is fixed
at compile time, dynamic memory allows programs to request memory as needed during
execution. This is essential when the size of the data structure isn't known beforehand, making
it flexible and efficient for certain applications.
malloc(size_t size) – Allocates a block of memory of specified size (in bytes) but does not
initialize it.
calloc(size_t n, size_t size) – Allocates memory for an array of n elements of size bytes each,
and initializes all bytes to zero.
realloc(void *ptr, size_t size) – Changes the size of the previously allocated memory block,
pointed to by ptr, to the new size.
free(void *ptr) – Deallocates the memory previously allocated using malloc, calloc, or realloc.
2. Heap Memory:
Dynamic memory is allocated from the heap, a large pool of memory reserved for this purpose.
Unlike the stack (used for static memory), the heap allows memory to be allocated and
deallocated in arbitrary order.
Many advanced data structures like linked lists, trees, and graphs require dynamic memory
allocation since their size can grow or shrink at runtime.
Linked List: Each node of a linked list is dynamically allocated as the list grows.
Binary Trees: Nodes are dynamically created when inserting new elements.
Hash Tables with Chaining: Each bucket in the hash table can grow dynamically as more
elements are inserted, with nodes dynamically allocated.
4. Fragmentation:
As memory is dynamically allocated and deallocated, fragmentation can occur, where free
memory is divided into small, non-contiguous blocks, making it harder to allocate large chunks
of memory.
5. Memory Leaks:
A memory leak occurs when memory is dynamically allocated but not deallocated (using free).
Over time, this can exhaust available memory, leading to performance degradation or crashes.
6. Garbage Collection:
Some languages, such as Java, Python, and C#, manage dynamic memory using automatic
garbage collection, which periodically reclaims memory that is no longer in use, preventing
memory leaks.
Efficient Use of Space: Only the required amount of memory is allocated at runtime.
8. Disadvantages:
Overhead: Dynamic memory allocation can introduce overhead compared to static memory
allocation, as it involves interaction with the system's memory management subsystem.
Dynamic memory management is crucial for implementing flexible and efficient data structures,
but it must be used with care to avoid common pitfalls such as fragmentation and memory leaks.
Garbage collection
Garbage collection in data structures refers to the process of automatically reclaiming memory
that is no longer in use by a program. In languages with automatic garbage collection (such as
Java, Python, and C#), the memory management system takes care of identifying and freeing
up memory that is no longer accessible, preventing memory leaks and ensuring efficient
memory utilization.
1. Garbage:
Garbage is memory that has been allocated but is no longer reachable by the program. For
example, if a data structure like a tree or list has nodes that are no longer referenced by any
variable, those nodes become "garbage."
2. Root Set:
The garbage collector identifies live (accessible) objects starting from a set of references called
the root set. The root set typically includes global variables, local variables in active stack
frames, and CPU registers. Any object not reachable from the root set is considered garbage
and can be reclaimed.
Reference Counting:
Each object maintains a count of how many references point to it. When this count drops to
zero, the object is garbage and can be deallocated.
Disadvantages: Cannot handle cyclic references (e.g., two objects referencing each other with
no external references, leading to memory leaks).
Mark-and-Sweep:
The garbage collector "marks" all objects that are reachable from the root set and "sweeps"
away all unmarked objects, reclaiming their memory.
Disadvantages: The program is paused during the mark-and-sweep process, which can cause
performance issues in large systems.
Objects are divided into generations based on their lifetime. Newly created objects are placed in
a young generation, and older objects that survive several rounds of collection are moved to an
older generation.
Advantages: Efficient, as most objects are short-lived and can be quickly collected, reducing the
overhead of scanning the entire heap.
Disadvantages: The overhead increases if there are many long-lived objects.
Copying Collection:
The memory is divided into two regions: an active region and a free region. The garbage
collector copies all live objects from the active region to the free region, compacting them in the
process. Once done, the free region becomes the new active region.
Disadvantages: Requires additional memory space and may have higher overhead for large
objects.
Garbage collection is important for dynamic data structures like linked lists, trees, and graphs,
where memory is frequently allocated and deallocated during runtime. Without garbage
collection, managing memory for these structures would require manual deallocation, which can
lead to memory leaks if forgotten.
For example, in a binary tree, when a node is deleted, the garbage collector can automatically
free the memory for the node and its associated children if they are no longer referenced
elsewhere.
Simplified Memory Management: Developers don’t have to manually track memory allocation
and deallocation, reducing the risk of memory leaks.
Safety: It helps prevent common issues like dangling pointers (pointers referring to freed
memory) and double-free errors (freeing memory that’s already been deallocated).
Better Productivity: Developers can focus on logic rather than memory management.
6. Disadvantages:
Overhead: Garbage collection can introduce runtime overhead, especially if large memory
scans are needed.
Pause Times: In some algorithms (like mark-and-sweep), the system may experience pauses
while the garbage collector reclaims memory. This can affect the performance of real-time
systems.
Less Control: Unlike manual memory management (e.g., using free() in C/C++), developers
have less control over when memory is deallocated, which can lead to less efficient memory
usage in some cases.
Java: Uses a generational garbage collector that combines mark-and-sweep with generational
techniques.
Python: Uses reference counting along with a cyclic garbage collector to handle cyclic
references.
C#: Utilizes the .NET garbage collector, which is generational and uses mark-and-sweep,
compacting techniques.
In C, garbage collection is not built into the language like in higher-level languages such as Java
or Python. C requires manual memory management, meaning that the programmer is
responsible for allocating and freeing memory explicitly using functions like malloc(), calloc(),
and free().
Memory Allocation in C:
malloc(size_t size): Allocates a block of memory of the specified size and returns a pointer to it.
The memory is not initialized.
calloc(size_t num, size_t size): Allocates memory for an array of num elements, each of size
bytes, and initializes the memory to zero.
free(void* ptr): Frees the memory block that was previously allocated by malloc(), calloc(), or
realloc().
Common Issues:
1. Memory Leaks: If memory is allocated using malloc() or calloc() but not freed using free(), it
results in a memory leak. Over time, this can cause the program to consume excessive memory
and possibly crash.
2. Dangling Pointers: If memory is freed but the pointer to that memory is still used, it can result
in accessing invalid memory. This is called a dangling pointer.
3. Double Free: Calling free() twice on the same pointer can lead to undefined behavior, which
may crash the program.
Since C does not automatically manage memory, it places the responsibility on developers to
carefully track memory usage, which can lead to errors.
Garbage collection in languages like Java handles this automatically by tracking references and
freeing memory that is no longer in use. In C, some external libraries (like the
Boehm-Demers-Weiser Garbage Collector) attempt to provide garbage collection for C
programs, but these are not part of the standard language and need to be explicitly integrated
into the project.
Set pointers to NULL after freeing them to avoid dangling pointer issues.
Use tools like Valgrind to detect memory leaks and other memory-related issues during
development.
By following good practices and using tools for memory debugging, developers can effectively
manage memory in C without relying on garbage collection.
In summary, garbage collection automates memory management, especially for dynamic data
structures, improving safety and productivity at the cost of some performance overhead.