数据结构基础(二):线性数据结构

**本篇文章的目的:**让初学者理解数组和链表的核心概念、C语言实现方式,以及它们的优缺点和适用场景。

1. 为什么需要数组和链表?

想象你要管理一个班级的学生成绩:

  • **场景1:**学生名单固定(如30人),且经常需要按学号快速查询成绩;
  • **场景2:**学生名单频繁变动(如插班、退学),且需要经常在中间插入或删除成绩。

问题:如何高效存储和操作这些数据?

答案:

  • **数组:**适合固定大小、随机访问的场景(如场景1)。
  • **链表:**适合动态大小、频繁插入/删除的场景(如场景2)。
2. 数组(Array)------ 连续存储的"书架"
2.1 概念
2.1.1 定义
  • 数组是一块连续的内存空间,用于存储相同类型的数据。
2.1.2 特点
  • 随机访问快:通过下标(索引)直接访问元素(如scores[2]);
  • 大小固定:创建时需指定长度(C语言中需手动管理内存);
  • 插入/删除慢:在中间插入或删除元素时,需移动其他元素。
2.2 C语言实现
2.2.1 静态数组(大小固定)
复制代码
#include <stdio.h>

int main() {
    int scores[5] = {90, 85, 78, 92, 88}; // 定义一个长度为5的数组
    print("第3位学生的成绩:%d\n", score[2]); // 输出78
    return 0;
}
2.2.2 动态数组(大小可变,需手动管理内存)
复制代码
#include <stdio.h>
#include <stdlib.h> // 包含malloc和free函数

int main() {
    int n;
    printf("输入学生人数:");
    scanf("%d", &n);

    int *scores = (int *)malloc(n * sizeof(int)); // 动态内存分配
    if (scores = NULL) {
        printf("内存分配失败!\n");
        return 1;
    }

    // 输入成绩
    for (int i = 0; i < n; i++) {
        printf("输入第%d个学生的成绩:", i + 1);
        scanf("%d", &scores[i]);
    }

    // 输出成绩
    for (int i = 0; i < n; i++){
        printf("第%d个学生的成绩:%d\n", i + 1, scores[i]);
    }

    free(scores); //释放内存
    return 0;
}
2.3 数组的优缺点
优点 缺点
随机访问快(O(1)时间) 大小固定,扩容成本高
内存连续,缓存友好 插入/删除需移动元素(O(n))
代码简单,易于实现 浪费空间(若分配过大)
2.4 适用场景
  • 数据大小已知且固定(如存储月份天数、棋盘格子)。
  • 需要频繁按索引访问数据。
3. 链表(Linked List)------ 灵活串联的"便签绳"
3.1 概念
3.1.1 定义
  • 链表由一系列节点组成,每个节点存储数据和指向下一个节点的指针。
3.1.2 特点
  • 动态大小:无需预先分配固定空间,可随时插入/删除节点。
  • 插入/删除快:只需修改指针,无需移动其他节点(O(1)时间、若已知位置)。
  • 随机访问慢:需从头结点开始遍历(O(n)时间)。
3.2 C语言实现示例
3.2.1 单链表(插入、删除、遍历)
复制代码
#include <stdio.h>
#include <stdlib.h>

typedef struct Node {
    int data;         // 数据域
    struct Node *next // 指针域,指向下一个结点
}

// 在链表尾部插入结点
void appendNode(Node **head, int value) {
    Node *newNode = (Node *)malloc(sizeof(Node));
    newNode->data = valud;
    newNode->next = NULL;

    if (*head == NULL) {
        *head = newNode;
    } else {
        Node *current = *head;
        while (current->next != NULL) {
            current = current->next;
        }
        current->next = newNode;
    }
}

// 删除指定值的结点
void deleteNode(Node **head, int value) {
    Node *temp = *head;
    Node *prev = NULL;

    if (temp != NULL && temp->data == value) {
        *head = temp->next; // 删除头结点
        free(temp);
        return;
    }

    while(temp != NULL && temp->data != value) {
        prev = temp;
        temp = temp->next;
    }

    if(temp == NULL) return; //未找到
    prev->next = temp->next;
    free(temp);
}

// 遍历链表
void printfList(Node *head){
    Node *current = head;
    while (current != NULL) {
        printf("%d -> ", current->data);
        current = current->next;
    }
    printf("NULL\n");
}

int main() {
    Node *head = NULL;

    appendNode(&head, 10);
    appendNode(&head, 20);
    appendNode(&head, 30);
    printfList(head); // 输出:10 -> 20 -> 30 -> NULL

    deleteNode(&head, 20);
    printfList(Head); // 输出:10 -> 30 -> NULL

    return 0;
}
3.3 链表的优缺点
优点 缺点
动态大小,无需预先分配 随机访问慢(O(n)时间)
插入/删除快(O(1)时间) 额外空间存储指针(每个节点多4字节)
不会浪费空间(按需分配) 代码复杂,易出错(如指针操作)
3.4 适用场景
  • 数据大小未知或频繁变动(如操作系统中的任务队列)。
  • 需要频繁在中间插入/删除数据(如音乐播放列表的顺序调整)。
4. 数组 VS 链表?(如何选择)
对比项 数组 链表
访问速度 快(直接索引) 慢(需遍历)
插入/删除速度 慢(需移动元素) 快(修改指针)
内存占用 连续内存,缓存友好 非连续内存,指针额外开销
实现难度 简单 较复杂(需管理指针)

选择建议

  • 用数组:数据大小固定、需要频繁随机访问(如游戏中的地图数据)。
  • 用链表:数据大小动态变化、需要频繁插入/删除(如浏览器历史记录)。
5. 拓展:动态数组和双向链表示例

5.1 实现一个动态数组:支持插入、删除、按索引访问元素。

复制代码
#include <stdio.h>
#include <stdlib.h>

// 动态数组结构体
typedef struct {
    int *data;      // 存储数组元素的指针
    int size;       // 当前元素数量
    int capacity;   // 当前分配的容量
} DynamicArray;

// 初始化动态数组
DynamicArray* create_array(int initial_capacity) {
    DynamicArray *arr = malloc(sizeof(DynamicArray));
    arr->data = malloc(initial_capacity * sizeof(int));
    arr->size = 0;
    arr->capacity = initial_capacity > 0 ? initial_capacity : 10;
    return arr;
}

// 调整数组容量
void resize_array(DynamicArray *arr, int new_capacity) {
    int *new_data = realloc(arr->data, new_capacity * sizeof(int));
    if (new_data) {
        arr->data = new_data;
        arr->capacity = new_capacity;
    } else {
        perror("Memory allocation failed");
        exit(EXIT_FAILURE);
    }
}

// 在指定位置插入元素
void insert_element(DynamicArray *arr, int index, int value) {
    // 边界检查
    if (index < 0 || index > arr->size) {
        fprintf(stderr, "Error: Index out of bounds\n");
        return;
    }

    // 扩容检查
    if (arr->size == arr->capacity) {
        resize_array(arr, arr->capacity * 2);
    }

    // 元素后移
    for (int i = arr->size; i > index; i--) {
        arr->data[i] = arr->data[i-1];
    }

    // 插入新元素
    arr->data[index] = value;
    arr->size++;
}

// 删除指定位置元素
void delete_element(DynamicArray *arr, int index) {
    if (index < 0 || index >= arr->size) {
        fprintf(stderr, "Error: Invalid delete index\n");
        return;
    }

    // 元素前移
    for (int i = index; i < arr->size - 1; i++) {
        arr->data[i] = arr->data[i+1];
    }

    arr->size--;
    arr->data[arr->size] = 0; // 清除旧值

    // 缩容检查
    if (arr->size > 0 && arr->size == arr->capacity / 4) {
        resize_array(arr, arr->capacity / 2);
    }
}

// 获取指定位置元素
int get_element(DynamicArray *arr, int index) {
    if (index < 0 || index >= arr->size) {
        fprintf(stderr, "Error: Invalid access index\n");
        return -1; // 实际使用中应返回错误码
    }
    return arr->data[index];
}

// 释放动态数组内存
void free_array(DynamicArray *arr) {
    free(arr->data);
    free(arr);
}

// 测试代码
int main() {
    printf("===== 动态数组测试 =====\n");
    DynamicArray *arr = create_array(3);
    
    // 测试插入
    insert_element(arr, 0, 10);
    insert_element(arr, 1, 20);
    insert_element(arr, 0, 5);  // 测试头部插入
    printf("插入后数组: ");
    for (int i = 0; i < arr->size; i++) {
        printf("%d ", get_element(arr, i));
    }
    
    // 测试删除
    delete_element(arr, 1);  // 删除中间元素
    printf("\n删除后数组: ");
    for (int i = 0; i < arr->size; i++) {
        printf("%d ", get_element(arr, i));
    }
    
    free_array(arr);
    return 0;
}
5.2 实现双向链表:每个结点有prev和next指针,支持前后遍历。
复制代码
#include <stdio.h>
#include <stdlib.h>

// 链表节点结构体
typedef struct Node {
    int value;
    struct Node *prev;
    struct Node *next;
} Node;

// 双向链表结构体
typedef struct {
    Node *head;
    Node *tail;
    int size;
} DoublyLinkedList;

// 创建新节点
Node* create_node(int value) {
    Node *node = malloc(sizeof(Node));
    node->value = value;
    node->prev = NULL;
    node->next = NULL;
    return node;
}

// 初始化链表
DoublyLinkedList* create_list() {
    DoublyLinkedList *list = malloc(sizeof(DoublyLinkedList));
    list->head = NULL;
    list->tail = NULL;
    list->size = 0;
    return list;
}

// 头部插入
void insert_head(DoublyLinkedList *list, int value) {
    Node *node = create_node(value);
    if (!list->head) {
        list->head = list->tail = node;
    } else {
        node->next = list->head;
        list->head->prev = node;
        list->head = node;
    }
    list->size++;
}

// 尾部插入
void insert_tail(DoublyLinkedList *list, int value) {
    Node *node = create_node(value);
    if (!list->tail) {
        list->head = list->tail = node;
    } else {
        node->prev = list->tail;
        list->tail->next = node;
        list->tail = node;
    }
    list->size++;
}

// 删除指定节点
void delete_node(DoublyLinkedList *list, Node *node) {
    if (!node || list->size == 0) return;

    if (node == list->head) {
        list->head = node->next;
    }
    if (node == list->tail) {
        list->tail = node->prev;
    }

    if (node->prev) node->prev->next = node->next;
    if (node->next) node->next->prev = node->prev;
    
    free(node);
    list->size--;
}

// 双向遍历测试
void traverse_forward(DoublyLinkedList *list) {
    printf("\n正向遍历: ");
    Node *current = list->head;
    while (current) {
        printf("%d ", current->value);
        current = current->next;
    }
}

void traverse_backward(DoublyLinkedList *list) {
    printf("\n反向遍历: ");
    Node *current = list->tail;
    while (current) {
        printf("%d ", current->value);
        current = current->prev;
    }
}

// 释放链表内存
void free_list(DoublyLinkedList *list) {
    Node *current = list->head;
    while (current) {
        Node *temp = current;
        current = current->next;
        free(temp);
    }
    free(list);
}

// 测试代码
int main() {
    printf("\n\n===== 双向链表测试 =====\n");
    DoublyLinkedList *list = create_list();
    
    // 测试插入
    insert_head(list, 30);
    insert_tail(list, 40);
    insert_tail(list, 50);
    
    traverse_forward(list);  // 30 40 50
    traverse_backward(list); // 50 40 30
    
    // 测试删除
    Node *toDelete = list->head->next;
    delete_node(list, toDelete);
    traverse_forward(list);  // 30 50
    
    free_list(list);
    return 0;
}
相关推荐
2401_841495642 小时前
【LeetCode刷题】爬楼梯
数据结构·python·算法·leetcode·动态规划·滑动窗口·斐波那契数列
风筝在晴天搁浅2 小时前
hot100 142.环形链表Ⅱ
数据结构·链表
好易学·数据结构3 小时前
可视化图解算法75:最长上升子序列(最长递增子序列)
数据结构·算法·leetcode·动态规划·力扣·牛客网
郝学胜-神的一滴3 小时前
Linux 多线程编程:深入理解 `pthread_join` 函数
linux·开发语言·jvm·数据结构·c++·程序人生·算法
客梦3 小时前
数据结构--排序
数据结构·笔记
embrace994 小时前
【数据结构学习】数据结构和算法
c语言·数据结构·c++·学习·算法·链表·哈希算法
杨恒984 小时前
GESPC++三级编程题 知识点
数据结构·c++·算法
历程里程碑4 小时前
LeetCode 283:原地移动零的优雅解法
java·c语言·开发语言·数据结构·c++·算法·leetcode
小高Baby@4 小时前
map的数据结构,扩容机制,key是无序的原因
数据结构·golang·哈希算法