After several weeks of C language learning, everyone has laid a certain foundation in programming. Starting today, we will move on to the next stage of learning ------ Data Structures.
1. Core Concepts
Program = Data Structure + Algorithm
- Data Structure: The structure of data objects manipulated by a program.
- Algorithm: The method by which a program manipulates data objects.
2. Metrics for Program Efficiency
Time Complexity
The proportional functional relationship between the growth of data volume and the growth of program running time is called the time complexity function, or Time Complexity for short.Common time complexities (in ascending order of efficiency):O(c), O(logn), O(n), O(nlogn), O(n²), O(n³), ..., O(2ⁿ)
Space Complexity
The proportional functional relationship between the growth of data volume and the growth of program memory usage is called Space Complexity.
3. Classification of Data Structures
3.1 Logical Structures
- Linear Structure: One-to-one logical relationship (e.g., sequence list, linked list).
- Tree Structure: One-to-many logical relationship (e.g., binary tree).
- Graph Structure: Many-to-many logical relationship (e.g., undirected graph, directed graph).
3.2 Storage Structures
Sequential Storage
Advantages:
- Convenient access to elements (via subscript offset).
Disadvantages:
- Low efficiency of insertion and deletion (requires element movement).
- Cannot utilize discrete small memory spaces.
Linked Storage
Advantages:
- High efficiency of insertion and deletion (only modify pointer references).
- Can make full use of discrete small memory spaces.
Disadvantages:
- Inconvenient element access (requires sequential traversal).
- Additional memory overhead for storing pointer fields.
Indexed Storage
Indexed storage is a storage method that creates an independent index table for the original data set ------ the index table records the key of the data and its physical address/position pointer in the original storage area. When searching for data, first find the address corresponding to the target key in the index table, then access the original data directly through the address instead of traversing the entire original data set.
Analogy: The table of contents of a book is the "index table", and the main text is the "original data set". To find a knowledge point, you first check the table of contents to get the page number (physical address), then turn to the corresponding page (direct access), instead of flipping through the book page by page.
Hashing Storage (Hash Table)
Hashing storage is a storage method that calculates the physical storage address of data directly from its key through a predefined hash function. When searching, the same hash function is used to calculate the key to get the address and access the data directly, without the need for an additional index table.
Analogy: The pickup code of a courier locker is the result of a hash function calculation. The key of the courier (data) is the waybill number. The courier company calculates the pickup code (storage address) through a hash function and places the courier in the corresponding locker. To pick up the courier, you enter the waybill number to recalculate the pickup code and open the locker directly, without checking any directory.
4. Key Content of Data Structure Learning
- Sequence List
- Linked List
- Sequential Stack
- Linked Stack
- Sequential Queue
- Linked Queue
- Binary Tree (Complete Binary Tree, Full Binary Tree)
- Hash Table
- Common Sorting and Searching Algorithms
5. Sequence List
5.1 Static Sequence List
Implemented with a fixed array in the stack/global area (e.g., int arr[100]), its capacity is fixed and immutable. It is only suitable for scenarios where the data volume is determined and is rarely used in actual development.
5.2 Dynamic Sequence List
Essentially equivalent to an array, it stores data by applying for heap memory and accesses all spaces through the start address + subscript offset.
Header File: seqlist.h
c
运行
#ifndef __SEQLIST_H
#define __SEQLIST_H
typedef int DataType;
// Create a dynamic sequence list with specified length
DataType *CreatSeqlist(int len);
// Destroy the dynamic sequence list and release heap memory
void DestorySeqlist(DataType **pparr);
#endif
Source File: seqlist.c
c
运行
#include <stdio.h>
#include <stdlib.h>
#include "seqlist.h"
DataType* CreatSeqlist (int len) {
DataType *pret = NULL;
pret = (DataType*)malloc(len * sizeof(DataType));
if(pret == NULL) {
perror("fail to malloc");
return NULL;
}
return pret;
}
void DestorySeqlist(DataType **pparr) {
free(*pparr);
*pparr = NULL;
return;
}
6. Linked List
6.1 Singly Linked List
A linked list is different from an array ------ array elements are stored in contiguous memory spaces, while the elements of a linked list (called nodes) are not necessarily contiguous. Each node contains a pointer that points to the next node, and we can access and traverse the linked list by storing the address of the next node in the previous one.
A Singly Linked List is a linked list that can only be traversed from the head to the tail (one-way traversal), and cannot be traversed in reverse.
Define the Node Structure
c
运行
typedef int DataType;
// Node structure of singly linked list
typedef struct ListNode {
DataType val; // Data field
struct ListNode *next; // Pointer field: point to the next node
} ListNode;
We rename the data type with typedef for code reusability ------ we can switch the data type of the linked list with just one line of code modification.
Singly linked lists are divided into headless linked lists and headed linked lists . Headless linked lists are cumbersome for traversal, search, insertion and other operations, so we first implement the headed linked list (with dummy node).
Basic Operations of Singly Linked List (with Dummy Node)
(1) Create a Dummy Node (Head Node)
c
运行
// Create a dummy node (no valid data, only as the head of the linked list)
ListNode *CreateDummyNode(void) {
ListNode *dummy = NULL;
dummy = (ListNode*)malloc(sizeof(ListNode));
if(dummy == NULL) { // Return NULL if memory application fails (avoid wild pointer)
perror("malloc dummy failed"); // Print error information for debugging
return NULL;
}
dummy->val = 0; // The value of dummy node is meaningless (set to 0 by default)
dummy->next = NULL; // Empty linked list: the successor of dummy node is NULL
return dummy;
}
(2) Head Insertion
c
运行
// Insert a new node at the head of the valid nodes (after the dummy node)
// Return 0 on success, -1 on failure
int InsertHeadNode(ListNode *dummy, DataType val) {
if(dummy == NULL) {
printf("error: dummy node is NULL\n");
return -1;
}
ListNode *newNode = (ListNode*)malloc(sizeof(ListNode));
if(newNode == NULL) {
perror("malloc newNode failed");
return -1;
}
newNode->val = val;
// Core logic: link to successor first, then to predecessor (avoid linked list breakage)
newNode->next = dummy->next;
dummy->next = newNode;
return 0;
}
(3) Tail Insertion
c
运行
// Insert a new node at the tail of the valid nodes
// Return 0 on success, -1 on failure
int InsertTailNode(ListNode *dummy, DataType val) {
if(dummy == NULL) {
printf("error: dummy node is NULL\n");
return -1;
}
ListNode *newNode = (ListNode*)malloc(sizeof(ListNode));
if(newNode == NULL) {
perror("malloc newNode failed");
return -1;
}
newNode->val = val;
newNode->next = NULL; // The tail node's successor must be NULL
ListNode *p = dummy;
// Traverse to find the last valid node
while(p->next != NULL) {
p = p->next;
}
p->next = newNode;
return 0;
}
(4) Insert at Specified Position
Insert a new node after the pos-th valid node (pos starts from 1).
c
运行
// Insert a new node after the pos-th valid node (pos starts from 1)
// Return 0 on success, -1 on failure
int InsertPosNode(ListNode *dummy, int pos, DataType val) {
if(dummy == NULL || pos < 1) {
printf("error: dummy node is NULL or pos is invalid\n");
return -1;
}
ListNode *p = dummy->next;
int curPos = 1;
// Traverse to find the pos-th valid node
while(p != NULL && curPos < pos) {
p = p->next;
curPos++;
}
if(p == NULL) {
printf("error: pos out of range\n");
return -1;
}
ListNode *newNode = (ListNode*)malloc(sizeof(ListNode));
if(newNode == NULL) {
perror("malloc newNode failed");
return -1;
}
newNode->val = val;
newNode->next = p->next;
p->next = newNode;
return 0;
}
(5) Delete Node by Specified Value
Delete the first valid node whose value is equal to the target value.
c
运行
// Delete the first valid node with the specified value
// Return 0 on success, -1 on failure
int DeleteNode(ListNode *dummy, DataType val) {
if(dummy == NULL) {
printf("error: dummy node is NULL\n");
return -1;
}
ListNode *pre = dummy; // Predecessor node (start from dummy node)
ListNode *cur = dummy->next; // Current node (start from the first valid node)
while(cur != NULL && cur->val != val) {
pre = cur;
cur = cur->next;
}
if(cur == NULL) {
printf("error: val %d not found\n", val);
return -1;
}
// Skip the node to be deleted
pre->next = cur->next;
// Release heap memory and avoid wild pointer
free(cur);
cur = NULL;
return 0;
}
(6) Traverse the Linked List
c
运行
// Traverse and print all valid nodes of the linked list
void TraverseLinkList(ListNode *dummy) {
if(dummy == NULL) {
printf("error: dummy node is NULL\n");
return;
}
ListNode *p = dummy->next;
if(p == NULL) {
printf("The linked list is empty\n");
return;
}
printf("Valid nodes of the linked list: ");
while(p != NULL) {
printf("%d ", p->val);
p = p->next;
}
printf("\n");
}
(7) Destroy the Linked List
c
运行
// Destroy the entire linked list (including dummy node) and release all heap memory
// Use double pointer to set the original dummy node pointer to NULL
void DestroyLinkList(ListNode **dummy) {
if(dummy == NULL || *dummy == NULL) return;
ListNode *p = *dummy;
ListNode *tmp = NULL;
// Traverse and release all nodes one by one
while(p != NULL) {
tmp = p->next;
free(p);
p = tmp;
}
// Set the original pointer to NULL to avoid wild pointer
*dummy = NULL;
}
(8) Find Node by Specified Value
c
运行
// Find the first valid node with the specified value
// Return the node pointer on success, NULL on failure
ListNode *FindNode(ListNode *dummy, DataType val) {
if(dummy == NULL) {
printf("error: dummy node is NULL\n");
return NULL;
}
ListNode *cur = dummy->next;
while(cur != NULL) {
if(cur->val == val) {
return cur;
}
cur = cur->next;
}
printf("info: val %d not found in list\n", val);
return NULL;
}
(9) Replace Node Value by Specified Value
Replace the value of the first valid node whose old value is oldVal with newVal.
c
运行
// Replace the first valid node with oldVal to newVal
// Return 0 on success, -1 on failure
int ReplaceNode(ListNode *dummy, DataType oldVal, DataType newVal) {
if(dummy == NULL) {
printf("error: dummy node is NULL\n");
return -1;
}
ListNode *target = FindNode(dummy, oldVal);
if(target != NULL) {
target->val = newVal;
printf("Success: replace %d with %d\n", oldVal, newVal);
return 0;
}
printf("Failed to replace: %d not found\n", oldVal);
return -1;
}
The above are the basic operations of the singly linked list with a dummy node. Familiarize yourselves with the code and practice more by writing it repeatedly, and you will master it gradually.
We will continue to learn more advanced content of data structures in the next class. See you then!