单链表(Singly Linked List)是一种基本的线性数据结构,它由一系列节点组成,每个节点包含数据和指向下一个节点的指针。在C语言中,通常使用动态内存分配来创建和管理单链表,下面详细介绍了单链表的结构定义及其常用操作。
单链表结构定义
单链表的节点结构定义如下:
typedef struct Node {
int data; // 节点数据域
struct Node* next; // 指向下一个节点的指针
} Node;
每个节点包含一个整型数据 data
和一个指向下一个节点的指针 next
。最后一个节点的 next
指针通常指向 NULL
,表示链表的结束。
单链表结构体
整个单链表的结构体定义如下:
typedef struct {
Node* head; // 头指针,指向头结点
int size; // 链表的节点数量
} LinkedList;
head
是指向链表头部的指针,实际上是指向一个空的头结点,而不是第一个数据节点。size
是链表中节点的数量,包括头结点。
常用操作详解
1. 创建链表和销毁链表
创建链表 createLinkedList()
:
LinkedList* createLinkedList() {
LinkedList* list = malloc(sizeof(LinkedList));
if (list == NULL) {
fprintf(stderr, "Memory allocation failed\n");
return NULL;
}
// 创建头结点
list->head = malloc(sizeof(Node));
if (list->head == NULL) {
fprintf(stderr, "Memory allocation failed\n");
free(list);
return NULL;
}
list->head->next = NULL; // 头结点初始时不指向任何节点
list->size = 0; // 初始时链表没有有效节点
return list;
}
createLinkedList()
函数动态分配内存创建一个空的链表,并初始化头结点和链表大小为0。
销毁链表 destroyLinkedList()
:
void destroyLinkedList(LinkedList* list) {
if (list == NULL) return;
Node* curr = list->head;
while (curr != NULL) {
Node* next = curr->next;
free(curr);
curr = next;
}
free(list);
}
destroyLinkedList()
函数释放链表的所有节点和头结点的内存空间,最后释放整个链表结构体的内存。
2. 插入操作
头部插入 insertAtHead()
:
void insertAtHead(LinkedList* list, int data) {
Node* newNode = malloc(sizeof(Node));
if (newNode == NULL) {
fprintf(stderr, "Memory allocation failed\n");
return;
}
newNode->data = data;
// 新节点指向当前的第一个节点
newNode->next = list->head->next;
// 头结点指向新节点,新节点成为新的第一个节点
list->head->next = newNode;
list->size++; // 链表大小加1
}
insertAtHead()
在链表头部插入一个新节点,新节点成为链表的第一个有效节点。
尾部插入 insertAtTail()
:
void insertAtTail(LinkedList* list, int data) {
Node* newNode = malloc(sizeof(Node));
if (newNode == NULL) {
fprintf(stderr, "Memory allocation failed\n");
return;
}
newNode->data = data;
newNode->next = NULL; // 新节点为尾节点,其 next 指针置为 NULL
Node* curr = list->head;
while (curr->next != NULL) {
curr = curr->next;
}
// curr 指向最后一个节点,将新节点链接到最后
curr->next = newNode;
list->size++; // 链表大小加1
}
insertAtTail()
在链表尾部插入一个新节点,新节点成为链表的最后一个有效节点。
指定位置插入 insertAtIndex()
:
void insertAtIndex(LinkedList* list, int data, int index) {
if (index < 0 || index > list->size) {
fprintf(stderr, "Invalid index\n");
return;
}
Node* newNode = malloc(sizeof(Node));
if (newNode == NULL) {
fprintf(stderr, "Memory allocation failed\n");
return;
}
newNode->data = data;
Node* curr = list->head;
for (int i = 0; i < index; i++) {
curr = curr->next;
}
// 新节点指向当前节点的下一个节点
newNode->next = curr->next;
// 当前节点指向新节点,新节点插入到当前位置
curr->next = newNode;
list->size++; // 链表大小加1
}
insertAtIndex()
在指定位置插入一个新节点,索引从0开始,0表示插入到头结点之后,1表示插入到第一个节点之后,依此类推。
3. 删除操作
头部删除 deleteAtHead()
:
void deleteAtHead(LinkedList* list) {
if (list->head->next == NULL) {
fprintf(stderr, "Empty list\n");
return;
}
Node* temp = list->head->next; // 保存第一个有效节点的指针
list->head->next = temp->next; // 头结点指向第二个节点
free(temp); // 释放第一个节点的内存
list->size--; // 链表大小减1
}
deleteAtHead()
删除链表的头部节点,即删除第一个有效节点。
尾部删除 deleteAtTail()
:
void deleteAtTail(LinkedList* list) {
if (list->head->next == NULL) {
fprintf(stderr, "Empty list\n");
return;
}
Node* curr = list->head;
while (curr->next->next != NULL) {
curr = curr->next;
}
Node* temp = curr->next; // 保存最后一个节点的指针
curr->next = NULL; // 倒数第二个节点的 next 指针置为 NULL
free(temp); // 释放最后一个节点的内存
list->size--; // 链表大小减1
}
deleteAtTail()
删除链表的尾部节点,即删除最后一个有效节点。
指定位置删除 deleteAtIndex()
:
void deleteAtIndex(LinkedList* list, int index) {
if (index < 0 || index >= list->size) {
fprintf(stderr, "Invalid index\n");
return;
}
Node* curr = list->head;
for (int i = 0; i < index; i++) {
curr = curr->next;
}
Node* temp = curr->next; // 保存要删除节点的指针
curr->next = temp->next; // 当前节点指向删除节点的下一个节点
free(temp); // 释放删除节点的内存
list->size--; // 链表大小减1
}
deleteAtIndex()
删除指定位置的节点,索引从0开始,0表示删除第一个有效节点,1表示删除第二个节点,依此类推。
4. 查找操作
按值查找 findByValue()
:
Node* findByValue(LinkedList* list, int data) {
Node* curr = list->head->next; // 从第一个有效节点开始查找
while (curr != NULL) {
if (curr->data == data) {
return curr; // 找到节点并返回指针
}
curr = curr->next; // 移动到下一个节点
}
return NULL; // 未找到返回 NULL
}
findByValue()
根据节点值查找节点,并返回节点的指针。如果找到则返回节点指针,否则返回NULL
。
按索引查找 findByIndex()
:
Node* findByIndex(LinkedList* list, int index) {
if (index < 0 || index >= list->size) {
fprintf(stderr, "Invalid index\n");
return NULL;
}
Node* curr = list->head->next; // 从第一个有效节点开始查找
for (int i = 0; i < index; i++) {
curr = curr->next;
}
return curr; // 返回找到的节点指针
}
findByIndex()
根据节点索引查找节点,并返回节点的指针。索引从0开始,0表示第一个有效节点,1表示第二个节点,依此类推。
5. 修改操作
修改节点数据 updateNode()
:
void updateNode(Node* node, int newData) {
if (node != NULL) {
node->data = newData; // 修改节点数据
}
}
updateNode()
修改指定节点的数据值为newData
。
6. 打印操作
打印链表 printLinkedList()
:
void printLinkedList(LinkedList* list) {
Node* curr = list->head->next; // 从第一个有效节点开始打印
while (curr != NULL) {
printf("%d -> ", curr->data);
curr = curr->next;
}
printf("NULL\n");
}
printLinkedList()
打印整个链表的节点数据,依次输出每个节点的数据值,以NULL
结尾表示链表结束。
示例代码
下面是一个使用上述定义的单链表操作的示例代码:
单链表结构定义和基本操作
LinkedList.h
#ifndef LINKEDLIST_H
#define LINKEDLIST_H
typedef struct Node {
int data;
struct Node* next;
} Node;
typedef struct {
Node* head; // 头指针,指向头结点
int size; // 链表大小(节点数量)
} LinkedList;
LinkedList* createLinkedList(); // 创建链表
void destroyLinkedList(LinkedList* list); // 销毁链表
void insertAtHead(LinkedList* list, int data); // 头部插入节点
void insertAtTail(LinkedList* list, int data); // 尾部插入节点
void insertAtIndex(LinkedList* list, int data, int index); // 指定位置插入节点
void deleteAtHead(LinkedList* list); // 删除头部节点
void deleteAtTail(LinkedList* list); // 删除尾部节点
void deleteAtIndex(LinkedList* list, int index); // 删除指定位置节点
Node* findByValue(LinkedList* list, int data); // 查找节点
Node* findByIndex(LinkedList* list, int index); // 根据索引查找节点
void printLinkedList(LinkedList* list); // 打印链表
#endif /* LINKEDLIST_H */
LinkedList.c
#include "LinkedList.h"
#include <stdio.h>
#include <stdlib.h>
// 创建链表
LinkedList* createLinkedList() {
LinkedList* list = malloc(sizeof(LinkedList));
if (list == NULL) {
fprintf(stderr, "Memory allocation failed\n");
return NULL;
}
// 创建头结点,并初始化链表大小为0
list->head = malloc(sizeof(Node));
if (list->head == NULL) {
fprintf(stderr, "Memory allocation failed\n");
free(list);
return NULL;
}
list->head->next = NULL;
list->size = 0;
return list;
}
// 销毁链表
void destroyLinkedList(LinkedList* list) {
if (list == NULL) return;
Node* curr = list->head;
while (curr != NULL) {
Node* next = curr->next;
free(curr);
curr = next;
}
free(list);
}
// 头部插入节点
void insertAtHead(LinkedList* list, int data) {
Node* newNode = malloc(sizeof(Node));
if (newNode == NULL) {
fprintf(stderr, "Memory allocation failed\n");
return;
}
newNode->data = data;
newNode->next = list->head->next; // 新节点指向原头结点的下一个节点
list->head->next = newNode; // 头结点指向新节点
list->size++;
}
// 尾部插入节点
void insertAtTail(LinkedList* list, int data) {
Node* newNode = malloc(sizeof(Node));
if (newNode == NULL) {
fprintf(stderr, "Memory allocation failed\n");
return;
}
newNode->data = data;
newNode->next = NULL;
Node* curr = list->head;
while (curr->next != NULL) {
curr = curr->next;
}
curr->next = newNode;
list->size++;
}
// 指定位置插入节点
void insertAtIndex(LinkedList* list, int data, int index) {
if (index < 0 || index > list->size) {
fprintf(stderr, "Invalid index\n");
return;
}
Node* newNode = malloc(sizeof(Node));
if (newNode == NULL) {
fprintf(stderr, "Memory allocation failed\n");
return;
}
newNode->data = data;
Node* curr = list->head;
for (int i = 0; i < index; i++) {
curr = curr->next;
}
newNode->next = curr->next;
curr->next = newNode;
list->size++;
}
// 删除头部节点
void deleteAtHead(LinkedList* list) {
if (list->head->next == NULL) {
fprintf(stderr, "Empty list\n");
return;
}
Node* temp = list->head->next;
list->head->next = temp->next;
free(temp);
list->size--;
}
// 删除尾部节点
void deleteAtTail(LinkedList* list) {
if (list->head->next == NULL) {
fprintf(stderr, "Empty list\n");
return;
}
Node* curr = list->head;
while (curr->next->next != NULL) {
curr = curr->next;
}
Node* temp = curr->next;
curr->next = NULL;
free(temp);
list->size--;
}
// 删除指定位置节点
void deleteAtIndex(LinkedList* list, int index) {
if (index < 0 || index >= list->size) {
fprintf(stderr, "Invalid index\n");
return;
}
Node* curr = list->head;
for (int i = 0; i < index; i++) {
curr = curr->next;
}
Node* temp = curr->next;
curr->next = temp->next;
free(temp);
list->size--;
}
// 根据节点值查找节点
Node* findByValue(LinkedList* list, int data) {
Node* curr = list->head->next;
while (curr != NULL) {
if (curr->data == data) {
return curr;
}
curr = curr->next;
}
return NULL;
}
// 根据索引查找节点
Node* findByIndex(LinkedList* list, int index) {
if (index < 0 || index >= list->size) {
fprintf(stderr, "Invalid index\n");
return NULL;
}
Node* curr = list->head->next;
for (int i = 0; i < index; i++) {
curr = curr->next;
}
return curr;
}
// 打印链表
void printLinkedList(LinkedList* list) {
Node* curr = list->head->next;
while (curr != NULL) {
printf("%d -> ", curr->data);
curr = curr->next;
}
printf("NULL\n");
}
示例程序演示链表的使用
main.c
#include <stdio.h>
#include "LinkedList.h"
int main() {
LinkedList* list = createLinkedList();
// 插入一些节点
insertAtTail(list, 1);
insertAtTail(list, 2);
insertAtTail(list, 3);
insertAtHead(list, 0);
printf("Linked List: ");
printLinkedList(list); // 打印链表:0 -> 1 -> 2 -> 3 -> NULL
// 删除节点
deleteAtTail(list);
deleteAtHead(list);
printf("Linked List after deletion: ");
printLinkedList(list); // 打印链表:1 -> 2 -> NULL
// 查找节点
Node* node = findByValue(list, 2);
if (node != NULL) {
printf("Found node with value 2\n");
} else {
printf("Node with value 2 not found\n");
}
// 销毁链表
destroyLinkedList(list);
return 0;
}
常用操作及其时间复杂度
-
头部插入 (
insertAtHead
):- 时间复杂度:O(1)
- 在链表头部插入节点只需修改几个指针的指向,操作的时间与链表的长度无关,因此是常数时间复杂度。
-
尾部插入 (
insertAtTail
):- 时间复杂度:O(n)
- 需要遍历链表找到尾节点,然后在尾节点后面插入新节点。因为需要遍历整个链表,所以时间复杂度为线性,与链表长度成正比。
-
指定位置插入 (
insertAtIndex
):- 时间复杂度:O(n)
- 需要先遍历找到指定位置的前一个节点,然后进行插入操作。因为需要遍历到指定位置,所以时间复杂度为线性。
-
头部删除 (
deleteAtHead
):- 时间复杂度:O(1)
- 删除头节点只需修改头指针,释放原头节点的内存,操作的时间与链表的长度无关,因此是常数时间复杂度。
-
尾部删除 (
deleteAtTail
):- 时间复杂度:O(n)
- 需要遍历链表找到尾节点的前一个节点,然后进行删除操作。因为需要遍历到尾节点前面的位置,所以时间复杂度为线性。
-
指定位置删除 (
deleteAtIndex
):- 时间复杂度:O(n)
- 需要先遍历找到指定位置的前一个节点,然后进行删除操作。因为需要遍历到指定位置,所以时间复杂度为线性。
-
按值查找 (
findByValue
):- 时间复杂度:O(n)
- 需要遍历整个链表来查找具有给定值的节点,因为需要考虑最坏情况下找到值在链表末尾,所以时间复杂度为线性。
-
按索引查找 (
findByIndex
):- 时间复杂度:O(n)
- 需要遍历整个链表来查找给定索引位置的节点,因为需要遍历到指定位置,所以时间复杂度为线性。
-
修改节点数据 (
updateNode
):- 时间复杂度:O(1)
- 直接修改节点的数据域,操作的时间与链表的长度无关,因此是常数时间复杂度。
总结
- 单链表的插入和删除操作的时间复杂度取决于操作位置,头部操作是 O(1),尾部和中间操作是 O(n)。
- 单链表的查找操作(按值和按索引)都是 O(n),因为需要遍历链表来寻找目标节点。
- 单链表适合于频繁插入、删除、查找的场景,但需要注意其插入和删除操作的时间复杂度与操作位置相关。