Lesson 24-Dynamic Data Structures

Linked List

A linked list is a linear data structure consisting of a series of nodes, where each node contains data and a pointer to the next node. The main advantages of linked lists are the efficiency of insertion and deletion operations, as they do not require moving other elements. Linked lists can be divided into singly linked lists, doubly linked lists, and circular linked lists.

Singly Linked List

A singly linked list is the simplest form of linked list, where each node contains only one pointer to the next node.

Node Definition

typedef struct Node {
    int data;
    struct Node* next;
} Node;

Create New Node

Node* createNode(int data) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    if (newNode == NULL) {
        printf("Memory allocation failed\n");
        exit(1);
    }
    newNode->data = data;
    newNode->next = NULL;
    return newNode;
}

Insert Node

Insert a node at the head of the linked list:

void insertAtHead(Node** head, int data) {
    Node* newNode = createNode(data);
    newNode->next = *head;
    *head = newNode;
}

Insert a node at the tail of the linked list:

void insertAtTail(Node** head, int data) {
    Node* newNode = createNode(data);
    if (*head == NULL) {
        *head = newNode;
        return;
    }
    Node* temp = *head;
    while (temp->next != NULL) {
        temp = temp->next;
    }
    temp->next = newNode;
}

Delete Node

Delete a specified node from the linked list:

void deleteNode(Node** head, int key) {
    Node* temp = *head;
    Node* prev = NULL;

    // If the head node is the one to be deleted
    if (temp != NULL && temp->data == key) {
        *head = temp->next;
        free(temp);
        return;
    }

    // Search for the node to be deleted
    while (temp != NULL && temp->data != key) {
        prev = temp;
        temp = temp->next;
    }

    // If the node is not found in the linked list
    if (temp == NULL) return;

    // Unlink and free memory
    prev->next = temp->next;
    free(temp);
}

Traverse Linked List

void printList(Node* head) {
    Node* temp = head;
    while (temp != NULL) {
        printf("%d -> ", temp->data);
        temp = temp->next;
    }
    printf("NULL\n");
}

Doubly Linked List

Each node in a doubly linked list contains two pointers: one to the previous node and one to the next node.

Node Definition

typedef struct DoublyNode {
    int data;
    struct DoublyNode* prev;
    struct DoublyNode* next;
} DoublyNode;

Create New Node

DoublyNode* createDoublyNode(int data) {
    DoublyNode* newNode = (DoublyNode*)malloc(sizeof(DoublyNode));
    if (newNode == NULL) {
        printf("Memory allocation failed\n");
        exit(1);
    }
    newNode->data = data;
    newNode->prev = NULL;
    newNode->next = NULL;
    return newNode;
}

Insert Node

Insert a node at the head of the doubly linked list:

void insertAtHeadDoubly(DoublyNode** head, int data) {
    DoublyNode* newNode = createDoublyNode(data);
    newNode->next = *head;
    if (*head != NULL) {
        (*head)->prev = newNode;
    }
    *head = newNode;
}

Insert a node at the tail of the doubly linked list:

void insertAtTailDoubly(DoublyNode** head, int data) {
    DoublyNode* newNode = createDoublyNode(data);
    if (*head == NULL) {
        *head = newNode;
        return;
    }
    DoublyNode* temp = *head;
    while (temp->next != NULL) {
        temp = temp->next;
    }
    temp->next = newNode;
    newNode->prev = temp;
}

Delete Node

Delete a specified node from the doubly linked list:

void deleteDoublyNode(DoublyNode** head, int key) {
    DoublyNode* temp = *head;

    // If the head node is the one to be deleted
    if (temp != NULL && temp->data == key) {
        *head = temp->next;
        if (*head != NULL) {
            (*head)->prev = NULL;
        }
        free(temp);
        return;
    }

    // Search for the node to be deleted
    while (temp != NULL && temp->data != key) {
        temp = temp->next;
    }

    // If the node is not found in the linked list
    if (temp == NULL) return;

    // Unlink and free memory
    if (temp->prev != NULL) {
        temp->prev->next = temp->next;
    }
    if (temp->next != NULL) {
        temp->next->prev = temp->prev;
    }
    free(temp);
}

Traverse Linked List

void printDoublyList(DoublyNode* head) {
    DoublyNode* temp = head;
    while (temp != NULL) {
        printf("%d <-> ", temp->data);
        temp = temp->next;
    }
    printf("NULL\n");
}

Circular Linked List

In a circular linked list, the last node points back to the head node, forming a ring.

Node Definition

The node definition for a circular linked list is the same as for a singly linked list:

typedef struct Node {
    int data;
    struct Node* next;
} Node;

Create Circular Linked List

Insert a node at the head of the circular linked list:

void insertAtHeadCircular(Node** head, int data) {
    Node* newNode = createNode(data);
    if (*head == NULL) {
        *head = newNode;
        newNode->next = *head;
    } else {
        Node* temp = *head;
        while (temp->next != *head) {
            temp = temp->next;
        }
        temp->next = newNode;
        newNode->next = *head;
        *head = newNode;
    }
}

Traverse circular linked list:

void printCircularList(Node* head) {
    if (head == NULL) return;
    Node* temp = head;
    do {
        printf("%d -> ", temp->data);
        temp = temp->next;
    } while (temp != head);
    printf("(back to head)\n");
}

Stack and Queue

Stacks and queues are two common linear data structures. A stack follows Last-In-First-Out (LIFO), while a queue follows First-In-First-Out (FIFO).

Stack

Array Implementation

#define MAX 100

typedef struct Stack {
    int items[MAX];
    int top;
} Stack;

void initializeStack(Stack* s) {
    s->top = -1;
}

int isFull(Stack* s) {
    return s->top == MAX - 1;
}

int isEmpty(Stack* s) {
    return s->top == -1;
}

void push(Stack* s, int value) {
    if (isFull(s)) {
        printf("Stack is full\n");
        return;
    }
    s->items[++(s->top)] = value;
}

int pop(Stack* s) {
    if (isEmpty(s)) {
        printf("Stack is empty\n");
        return -1;
    }
    return s->items[(s->top)--];
}

int peek(Stack* s) {
    if (isEmpty(s)) {
        printf("Stack is empty\n");
        return -1;
    }
    return s->items[s->top];
}

Linked List Implementation

typedef struct StackNode {
    int data;
    struct StackNode* next;
} StackNode;

typedef struct Stack {
    StackNode* top;
} Stack;

void initializeStack(Stack* s) {
    s->top = NULL;
}

int isEmpty(Stack* s) {
    return s->top == NULL;
}

void push(Stack* s, int value) {
    StackNode* newNode = createNode(value);
    newNode->next = s->top;
    s->top = newNode;
}

int pop(Stack* s) {
    if (isEmpty(s)) {
        printf("Stack is empty\n");
        return -1;
    }
    StackNode* temp = s->top;
    int value = temp->data;
    s->top = s->top->next;
    free(temp);
    return value;
}

int peek(Stack* s) {
    if (isEmpty(s)) {
        printf("Stack is empty\n");
        return -1;
    }
    return s->top->data;
}

Queue

Array Implementation

#define MAX 100

typedef struct Queue {
    int items[MAX];
    int front;
    int rear;
} Queue;

void initializeQueue(Queue* q) {
    q->front = -1;
    q->rear = -1;
}

int isEmpty(Queue* q) {
    return q->front == -1;
}

int isFull(Queue* q) {
    return (q->rear + 1) % MAX == q->front;
}

void enqueue(Queue* q, int value) {
    if (isFull(q)) {
        printf("Queue is full\n");
        return;
    }
    if (isEmpty(q)) {
        q->front = 0;
    }
    q->rear = (q->rear + 1) % MAX;
    q->items[q->rear] = value;
}

int dequeue(Queue* q) {
    if (isEmpty(q)) {
        printf("Queue is empty\n");
        return -1;
    }
    int item = q->items[q->front];
    if (q->front == q->rear) {
        q->front = q->rear = -1;
    } else {
        q->front = (q->front + 1) % MAX;
    }
    return item;
}

int peek(Queue* q) {
    if (isEmpty(q)) {
        printf("Queue is empty\n");
        return -1;
    }
    return q->items[q->front];
}

Linked List Implementation

typedef struct QueueNode {
    int data;
    struct QueueNode* next;
} QueueNode;

typedef struct Queue {
    QueueNode* front;
    QueueNode* rear;
} Queue;

void initializeQueue(Queue* q) {
    q->front = q->rear = NULL;
}

int isEmpty(Queue* q) {
    return q->front == NULL;
}

void enqueue(Queue* q, int value) {
    QueueNode* newNode = createNode(value);
    if (q->rear == NULL) {
        q->front = q->rear = newNode;
        return;
    }
    q->rear->next = newNode;
    q->rear = newNode;
}

int dequeue(Queue* q) {
    if (isEmpty(q)) {
        printf("Queue is empty\n");
        return -1;
    }
    QueueNode* temp = q->front;
    int value = temp->data;
    q->front = q->front->next;
    if (q->front == NULL) {
        q->rear = NULL;
    }
    free(temp);
    return value;
}

int peek(Queue* q) {
    if (isEmpty(q)) {
        printf("Queue is empty\n");
        return -1;
    }
    return q->front->data;
}

Tree

A tree is a non-linear data structure consisting of nodes and edges connecting the nodes. Common tree structures include binary trees, AVL trees, and red-black trees.

Binary Tree

Node Definition

typedef struct TreeNode {
    int data;
    struct TreeNode* left;
    struct TreeNode* right;
} TreeNode;

TreeNode* createTreeNode(int data) {
    TreeNode* newNode = (TreeNode*)malloc(sizeof(TreeNode));
    if (newNode == NULL) {
        printf("Memory allocation failed\n");
        exit(1);
    }
    newNode->data = data;
    newNode->left = newNode->right = NULL;
    return newNode;
}

Insert Node

Insertion in a binary tree usually requires recursive implementation:

TreeNode* insertTreeNode(TreeNode* root, int data) {
    if (root == NULL) {
        return createTreeNode(data);
    }
    if (data < root->data) {
        root->left = insertTreeNode(root->left, data);
    } else if (data > root->data) {
        root->right = insertTreeNode(root->right, data);
    }
    return root;
}

Traverse Binary Tree

  • Pre-order Traversal: Root -> Left -> Right
void preOrderTraversal(TreeNode* root) {
    if (root != NULL) {
        printf("%d ", root->data);
        preOrderTraversal(root->left);
        preOrderTraversal(root->right);
    }
}
  • In-order Traversal: Left -> Root -> Right
void inOrderTraversal(TreeNode* root) {
    if (root != NULL) {
        inOrderTraversal(root->left);
        printf("%d ", root->data);
        inOrderTraversal(root->right);
    }
}
  • Post-order Traversal: Left -> Right -> Root
void postOrderTraversal(TreeNode* root) {
    if (root != NULL) {
        postOrderTraversal(root->left);
        postOrderTraversal(root->right);
        printf("%d ", root->data);
    }
}

AVL Tree

An AVL tree is a self-balancing binary search tree that maintains balance through rotation operations.

Node Definition

typedef struct AVLNode {
    int data;
    struct AVLNode* left;
    struct AVLNode* right;
    int height;
} AVLNode;

AVLNode* createAVLNode(int data) {
    AVLNode* newNode = (AVLNode*)malloc(sizeof(AVLNode));
    if (newNode == NULL) {
        printf("Memory allocation failed\n");
        exit(1);
    }
    newNode->data = data;
    newNode->left = newNode->right = NULL;
    newNode->height = 1;
    return newNode;
}

Get Node Height

int getHeight(AVLNode* node) {
    if (node == NULL) return 0;
    return node->height;
}

Get Balance Factor

int getBalanceFactor(AVLNode* node) {
    if (node == NULL) return 0;
    return getHeight(node->left) - getHeight(node->right);
}

Update Node Height

void updateHeight(AVLNode* node) {
    if (node == NULL) return;
    node->height = 1 + (getHeight(node->left) > getHeight(node->right) ? getHeight(node->left) : getHeight(node->right));
}

Right Rotation

AVLNode* rightRotate(AVLNode* y) {
    AVLNode* x = y->left;
    AVLNode* T2 = x->right;

    // Perform rotation
    x->right = y;
    y->left = T2;

    // Update heights
    updateHeight(y);
    updateHeight(x);

    return x;
}

Left Rotation

AVLNode* leftRotate(AVLNode* x) {
    AVLNode* y = x->right;
    AVLNode* T2 = y->left;

    // Perform rotation
    y->left = x;
    x->right = T2;

    // Update heights
    updateHeight(x);
    updateHeight(y);

    return y;
}

Insert Node

AVLNode* insertAVLNode(AVLNode* node, int data) {
    if (node == NULL) return createAVLNode(data);

    if (data < node->data) {
        node->left = insertAVLNode(node->left, data);
    } else if (data > node->data) {
        node->right = insertAVLNode(node->right, data);
    } else {
        return node; // Duplicates not allowed
    }

    // Update height
    updateHeight(node);

    // Get balance factor
    int balance = getBalanceFactor(node);

    // Left-Left case
    if (balance > 1 && data < node->left->data) {
        return rightRotate(node);
    }

    // Right-Right case
    if (balance < -1 && data > node->right->data) {
        return leftRotate(node);
    }

    // Left-Right case
    if (balance > 1 && data > node->left->data) {
        node->left = leftRotate(node->left);
        return rightRotate(node);
    }

    // Right-Left case
    if (balance < -1 && data < node->right->data) {
        node->right = rightRotate(node->right);
        return leftRotate(node);
    }

    return node;
}

Red-Black Tree

A red-black tree is a self-balancing binary search tree that maintains balance through color marking and rotation operations. The rules for red-black trees are complex; only the basic definition and insertion operation are provided here.

Node Definition

typedef enum { RED, BLACK } Color;

typedef struct RBNode {
    int data;
    Color color;
    struct RBNode* left;
    struct RBNode* right;
    struct RBNode* parent;
} RBNode;

RBNode* createRBNode(int data) {
    RBNode* newNode = (RBNode*)malloc(sizeof(RBNode));
    if (newNode == NULL) {
        printf("Memory allocation failed\n");
        exit(1);
    }
    newNode->data = data;
    newNode->color = RED;
    newNode->left = newNode->right = newNode->parent = NULL;
    return newNode;
}

Insert Node

The insertion operation for a red-black tree is complex and requires handling multiple cases; only the basic framework is provided here:

void insertRBNode(RBNode** root, int data) {
    RBNode* newNode = createRBNode(data);
    // Basic insertion logic (similar to binary search tree)
    // Then perform red-black tree fix-up operations
    // Fix-up includes color adjustments and rotations
}

Graph

A graph is a non-linear data structure consisting of vertices and edges. Common representation methods include adjacency lists and adjacency matrices.

Adjacency List

An adjacency list uses linked lists to represent the edges of a graph.

Graph Definition

typedef struct AdjListNode {
    int dest;
    struct AdjListNode* next;
} AdjListNode;

typedef struct AdjList {
    AdjListNode* head;
} AdjList;

typedef struct Graph {
    int numVertices;
    AdjList* array;
} Graph;

AdjListNode* createAdjListNode(int dest) {
    AdjListNode* newNode = (AdjListNode*)malloc(sizeof(AdjListNode));
    newNode->dest = dest;
    newNode->next = NULL;
    return newNode;
}

Graph* createGraph(int vertices) {
    Graph* graph = (Graph*)malloc(sizeof(Graph));
    graph->numVertices = vertices;
    graph->array = (AdjList*)malloc(vertices * sizeof(AdjList));
    for (int i = 0; i < vertices; ++i)
        graph->array[i].head = NULL;
    return graph;
}

void addEdge(Graph* graph, int src, int dest) {
    AdjListNode* newNode = createAdjListNode(dest);
    newNode->next = graph->array[src].head;
    graph->array[src].head = newNode;

    // For undirected graph, add reverse edge
    // newNode = createAdjListNode(src);
    // newNode->next = graph->array[dest].head;
    // graph->array[dest].head = newNode;
}

Traverse Graph

  • Depth-First Search (DFS)
void DFSUtil(Graph* graph, int v, int visited[]) {
    visited[v] = 1;
    printf("%d ", v);

    AdjListNode* temp = graph->array[v].head;
    while (temp) {
        int adjVertex = temp->dest;
        if (!visited[adjVertex]) {
            DFSUtil(graph, adjVertex, visited);
        }
        temp = temp->next;
    }
}

void DFS(Graph* graph, int startVertex) {
    int* visited = (int*)calloc(graph->numVertices, sizeof(int));
    DFSUtil(graph, startVertex, visited);
    free(visited);
}
  • Breadth-First Search (BFS)
void BFS(Graph* graph, int startVertex) {
    int* visited = (int*)calloc(graph->numVertices, sizeof(int));
    int queue[MAX], front = 0, rear = 0;

    visited[startVertex] = 1;
    queue[rear++] = startVertex;

    while (front < rear) {
        int currentVertex = queue[front++];
        printf("%d ", currentVertex);

        AdjListNode* temp = graph->array[currentVertex].head;
        while (temp) {
            int adjVertex = temp->dest;
            if (!visited[adjVertex]) {
                visited[adjVertex] = 1;
                queue[rear++] = adjVertex;
            }
            temp = temp->next;
        }
    }
    free(visited);
}

Adjacency Matrix

An adjacency matrix uses a two-dimensional array to represent the edges of a graph.

Graph Definition

typedef struct GraphMatrix {
    int numVertices;
    int** matrix;
} GraphMatrix;

GraphMatrix* createGraphMatrix(int vertices) {
    GraphMatrix* graph = (GraphMatrix*)malloc(sizeof(GraphMatrix));
    graph->numVertices = vertices;
    graph->matrix = (int**)malloc(vertices * sizeof(int*));
    for (int i = 0; i < vertices; i++) {
        graph->matrix[i] = (int*)malloc(vertices * sizeof(int));
        for (int j = 0; j < vertices; j++) {
            graph->matrix[i][j] = 0;
        }
    }
    return graph;
}

void addEdgeMatrix(GraphMatrix* graph, int src, int dest) {
    graph->matrix[src][dest] = 1;
    // For undirected graph, add reverse edge
    // graph->matrix[dest][src] = 1;
}

Traverse Graph

  • Depth-First Search (DFS)
void DFSMatrixUtil(GraphMatrix* graph, int v, int visited[]) {
    visited[v] = 1;
    printf("%d ", v);

    for (int i = 0; i < graph->numVertices; i++) {
        if (graph->matrix[v][i] && !visited[i]) {
            DFSMatrixUtil(graph, i, visited);
        }
    }
}

void DFSMatrix(GraphMatrix* graph, int startVertex) {
    int* visited = (int*)calloc(graph->numVertices, sizeof(int));
    DFSMatrixUtil(graph, startVertex, visited);
    free(visited);
}
  • Breadth-First Search (BFS)
void BFSMatrix(GraphMatrix* graph, int startVertex) {
    int* visited = (int*)calloc(graph->numVertices, sizeof(int));
    int queue[MAX], front = 0, rear = 0;

    visited[startVertex] = 1;
    queue[rear++] = startVertex;

    while (front < rear) {
        int currentVertex = queue[front++];
        printf("%d ", currentVertex);

        for (int i = 0; i < graph->numVertices; i++) {
            if (graph->matrix[currentVertex][i] && !visited[i]) {
                visited[i] = 1;
                queue[rear++] = i;
            }
        }
    }
    free(visited);
}

Heap

A heap is a special complete binary tree, commonly used to implement priority queues. Heaps are divided into max-heaps and min-heaps.

Max-Heap

In a max-heap, the value of each node is greater than or equal to the values of its child nodes.

Heap Definition

typedef struct MaxHeap {
    int* array;
    int capacity;
    int size;
} MaxHeap;

MaxHeap* createMaxHeap(int capacity) {
    MaxHeap* maxHeap = (MaxHeap*)malloc(sizeof(MaxHeap));
    maxHeap->capacity = capacity;
    maxHeap->size = 0;
    maxHeap->array = (int*)malloc(capacity * sizeof(int));
    return maxHeap;
}

Insert Element

void swap(int* a, int* b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

void heapifyUp(MaxHeap* maxHeap, int index) {
    while (index > 0 && maxHeap->array[(index - 1) / 2] < maxHeap->array[index]) {
        swap(&maxHeap->array[(index - 1) / 2], &maxHeap->array[index]);
        index = (index - 1) / 2;
    }
}

void insertMaxHeap(MaxHeap* maxHeap, int value) {
    if (maxHeap->size == maxHeap->capacity) {
        printf("Heap is full\n");
        return;
    }
    maxHeap->array[maxHeap->size] = value;
    heapifyUp(maxHeap, maxHeap->size);
    maxHeap->size++;
}

Delete Root Element

void heapifyDown(MaxHeap* maxHeap, int index) {
    int largest = index;
    int left = 2 * index + 1;
    int right = 2 * index + 2;

    if (left < maxHeap->size && maxHeap->array[left] > maxHeap->array[largest]) {
        largest = left;
    }

    if (right < maxHeap->size && maxHeap->array[right] > maxHeap->array[largest]) {
        largest = right;
    }

    if (largest != index) {
        swap(&maxHeap->array[index], &maxHeap->array[largest]);
        heapifyDown(maxHeap, largest);
    }
}

int extractMax(MaxHeap* maxHeap) {
    if (maxHeap->size <= 0) {
        return -1;
    }
    if (maxHeap->size == 1) {
        maxHeap->size--;
        return maxHeap->array[0];
    }

    int root = maxHeap->array[0];
    maxHeap->array[0] = maxHeap->array[maxHeap->size - 1];
    maxHeap->size--;
    heapifyDown(maxHeap, 0);

    return root;
}

Min-Heap

In a min-heap, the value of each node is less than or equal to the values of its child nodes. The implementation of a min-heap is similar to a max-heap, only the comparison logic in heapifyUp and heapifyDown needs to be adjusted.

Insert Element

void heapifyUpMin(MaxHeap* minHeap, int index) {
    while (index > 0 && minHeap->array[(index - 1) / 2] > minHeap->array[index]) {
        swap(&minHeap->array[(index - 1) / 2], &minHeap->array[index]);
        index = (index - 1) / 2;
    }
}

void insertMinHeap(MaxHeap* minHeap, int value) {
    if (minHeap->size == minHeap->capacity) {
        printf("Heap is full\n");
        return;
    }
    minHeap->array[minHeap->size] = value;
    heapifyUpMin(minHeap, minHeap->size);
    minHeap->size++;
}

Delete Root Element

void heapifyDownMin(MaxHeap* minHeap, int index) {
    int smallest = index;
    int left = 2 * index + 1;
    int right = 2 * index + 2;

    if (left < minHeap->size && minHeap->array[left] < minHeap->array[smallest]) {
        smallest = left;
    }

    if (right < minHeap->size && minHeap->array[right] < minHeap->array[smallest]) {
        smallest = right;
    }

    if (smallest != index) {
        swap(&minHeap->array[index], &minHeap->array[smallest]);
        heapifyDownMin(minHeap, smallest);
    }
}

int extractMin(MaxHeap* minHeap) {
    if (minHeap->size <= 0) {
        return -1;
    }
    if (minHeap->size == 1) {
        minHeap->size--;
        return minHeap->array[0];
    }

    int root = minHeap->array[0];
    minHeap->array[0] = minHeap->array[minHeap->size - 1];
    minHeap->size--;
    heapifyDownMin(minHeap, 0);

    return root;
}

Heap Sort

Heap sort is a sorting algorithm based on heaps with a time complexity of O(n log n).

Heap Sort Implementation

void heapifyForSort(int* array, int size, int index) {
    int largest = index;
    int left = 2 * index + 1;
    int right = 2 * index + 2;

    if (left < size && array[left] > array[largest]) {
        largest = left;
    }

    if (right < size && array[right] > array[largest]) {
        largest = right;
    }

    if (largest != index) {
        swap(&array[index], &array[largest]);
        heapifyForSort(array, size, largest);
    }
}

void heapSort(int* array, int size) {
    // Build max heap
    for (int i = size / 2 - 1; i >= 0; i--) {
        heapifyForSort(array, size, i);
    }

    // Extract elements and adjust heap
    for (int i = size - 1; i > 0; i--) {
        swap(&array[0], &array[i]);
        heapifyForSort(array, i, 0);
    }
}

Summary

This article provides a detailed introduction to dynamic data structures in C, including linked lists, stacks and queues, trees, graphs, and heaps. For each data structure, basic definitions, operations, and implementation code are provided. Through this content, readers can gain a deep understanding of the principles and application scenarios of these data structures and apply them flexibly in actual programming.

Share your love