数据结构概述

在程序设计中,数据结构的选择直接影响算法的效率和代码的可维护性。本文将深入解析顺序表、单链表、双向链表、栈、链栈、队列、循环队列这七种线性数据结构,通过对比它们的结构体定义和函数实现,帮助大家理解各自的特点和适用场景。

1. 顺序表(Sequence List)

结构体定义与特点

#include 
#include 
#define MAXSIZE 100
typedef struct {
    int data[MAXSIZE];  // 静态数组存储元素
    int length;         // 当前长度
} SeqList;

结构特点

使用连续内存空间                通过数组下标直接访问元素                需要预先分配固定大小

核心函数实现对比

// 初始化顺序表
void initSeqList(SeqList* list) {
    list->length = 0;  // 只需设置长度为0
}
// 插入元素
int insertSeqList(SeqList* list, int pos, int elem) {
    if (pos < 1 || pos > list->length + 1) return 0;
    if (list->length >= MAXSIZE) return 0;
    for (int i = list->length; i >= pos; i--) {
        list->data[i] = list->data[i - 1];  // 移动后续元素
    }
    list->data[pos - 1] = elem;
    list->length++;
    return 1;
}
// 删除元素
int deleteSeqList(SeqList* list, int pos) {
    if (pos < 1 || pos > list->length) return 0;
    for (int i = pos; i < list->length; i++) {
        list->data[i - 1] = list->data[i];  // 前移覆盖
    }
    list->length--;
    return 1;
}

函数说明表

函数名头文件参数返回值参数示例示例含义
initSeqListstdio.h, stdlib.hlist: 顺序表指针&list初始化空顺序表
insertSeqListstdio.h, stdlib.hlist: 顺序表指针
pos: 位置
elem: 元素
成功1,失败0&list, 2, 10在位置2插入10
deleteSeqListstdio.h, stdlib.hlist: 顺序表指针
pos: 位置
成功1,失败0&list, 3删除位置3元素

2. 单链表(Singly Linked List)

结构体定义与特点

#include 
#include 
typedef struct Node {
    int data;           // 数据域
    struct Node* next;  // 指针域
} ListNode;

结构特点

节点包含数据和指针                内存非连续,通过指针链接                动态内存分配

核心函数实现对比

// 创建新节点
ListNode* createNode(int elem) {
    ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
    newNode->data = elem;
    newNode->next = NULL;  // 新节点next初始为NULL
    return newNode;
}
// 插入节点
void insertListNode(ListNode** head, int pos, int elem) {
    ListNode* newNode = createNode(elem);
    if (pos == 1) {  // 头插法
        newNode->next = *head;
        *head = newNode;
        return;
    }
    ListNode* current = *head;
    for (int i = 1; i < pos - 1 && current != NULL; i++) {
        current = current->next;  // 遍历找到插入位置
    }
    if (current == NULL) return;
    newNode->next = current->next;  // 新节点指向原后继
    current->next = newNode;        // 原前驱指向新节点
}
// 删除节点
void deleteListNode(ListNode** head, int pos) {
    if (*head == NULL) return;
    ListNode* temp = *head;
    if (pos == 1) {  // 删除头节点
        *head = temp->next;
        free(temp);
        return;
    }
    for (int i = 1; temp != NULL && i < pos - 1; i++) {
        temp = temp->next;  // 找到删除位置的前驱
    }
    if (temp == NULL || temp->next == NULL) return;
    ListNode* toDelete = temp->next;    // 要删除的节点
    temp->next = toDelete->next;        // 前驱指向后继
    free(toDelete);                     // 释放内存
}

函数说明表

函数名头文件参数返回值参数示例示例含义
createNodestdio.h, stdlib.helem: 节点数据新节点指针5创建数据为5的节点
insertListNodestdio.h, stdlib.hhead: 头指针地址
pos: 位置
elem: 元素
&head, 1, 8在头部插入8
deleteListNodestdio.h, stdlib.hhead: 头指针地址
pos: 位置
&head, 2删除第2个节点

3. 双向链表(Doubly Linked List)

结构体定义与特点

#include 
#include 
typedef struct DNode {
    int data;               // 数据域
    struct DNode* prev;     // 前驱指针
    struct DNode* next;     // 后继指针
} DListNode;

结构特点

每个节点有两个指针                可以双向遍历                插入删除操作更复杂但更灵活

核心函数实现对比

// 创建双向节点
DListNode* createDNode(int elem) {
    DListNode* newNode = (DListNode*)malloc(sizeof(DListNode));
    newNode->data = elem;
    newNode->prev = NULL;  // 两个指针都初始化为NULL
    newNode->next = NULL;
    return newNode;
}
// 插入节点(双向链表特有)
void insertDListNode(DListNode** head, int pos, int elem) {
    DListNode* newNode = createDNode(elem);
    if (*head == NULL) {  // 空链表
        *head = newNode;
        return;
    }
    if (pos == 1) {  // 头插
        newNode->next = *head;
        (*head)->prev = newNode;  // 设置原头节点的前驱
        *head = newNode;
        return;
    }
    DListNode* current = *head;
    for (int i = 1; i < pos - 1 && current != NULL; i++) {
        current = current->next;
    }
    if (current == NULL) return;
    newNode->next = current->next;
    newNode->prev = current;           // 设置新节点前驱
    if (current->next != NULL) {
        current->next->prev = newNode; // 设置后继节点的前驱
    }
    current->next = newNode;           // 设置前驱节点的后继
}
// 删除节点(双向链表特有)
void deleteDListNode(DListNode** head, int pos) {
    if (*head == NULL) return;
    DListNode* current = *head;
    if (pos == 1) {  // 删除头节点
        *head = current->next;
        if (*head != NULL) {
            (*head)->prev = NULL;  // 新头节点前驱置空
        }
        free(current);
        return;
    }
    for (int i = 1; current != NULL && i < pos; i++) {
        current = current->next;  // 直接找到要删除的节点
    }
    if (current == NULL) return;
    if (current->next != NULL) {
        current->next->prev = current->prev;  // 更新后继的前驱
    }
    if (current->prev != NULL) {
        current->prev->next = current->next;  // 更新前驱的后继
    }
    free(current);
}

4. 栈(Stack)- 顺序实现

结构体定义与特点

#include 
#include 
#define STACK_SIZE 100
typedef struct {
    int data[STACK_SIZE];  // 数组存储
    int top;               // 栈顶指针
} SeqStack;

结构特点

LIFO(后进先出)                只能在栈顶操作                顺序存储实现

核心函数实现对比

// 初始化栈
void initStack(SeqStack* stack) {
    stack->top = -1;  // 栈空标志
}
// 入栈
int pushStack(SeqStack* stack, int elem) {
    if (stack->top >= STACK_SIZE - 1) return 0;
    stack->data[++stack->top] = elem;  // 栈顶上移再赋值
    return 1;
}
// 出栈
int popStack(SeqStack* stack) {
    if (stack->top < 0) return -1;
    return stack->data[stack->top--];  // 返回栈顶元素再下移
}
// 获取栈顶(顺序栈特有)
int peekStack(SeqStack* stack) {
    if (stack->top < 0) return -1;
    return stack->data[stack->top];  // 只读不修改top
}

函数说明表

函数名头文件参数返回值参数示例示例含义
initStackstdio.h, stdlib.hstack: 栈指针&stack初始化空栈
pushStackstdio.h, stdlib.hstack: 栈指针
elem: 元素
成功1,失败0&stack, 55入栈
popStackstdio.h, stdlib.hstack: 栈指针栈顶元素&stack弹出栈顶
peekStackstdio.h, stdlib.hstack: 栈指针栈顶元素&stack查看栈顶

5. 链栈(Linked Stack)

结构体定义与特点

#include 
#include 
typedef struct StackNode {
    int data;                   // 数据域
    struct StackNode* next;     // 指针域
} LinkStack;

结构特点

栈顶是链表头                动态内存管理                理论上容量无限

核心函数实现对比

// 入栈(链栈特有)
void pushLinkStack(LinkStack** top, int elem) {
    LinkStack* newNode = (LinkStack*)malloc(sizeof(LinkStack));
    newNode->data = elem;
    newNode->next = *top;  // 新节点指向原栈顶
    *top = newNode;        // 更新栈顶指针
}
// 出栈(链栈特有)
int popLinkStack(LinkStack** top) {
    if (*top == NULL) return -1;
    LinkStack* temp = *top;
    int elem = temp->data;
    *top = (*top)->next;  // 栈顶下移
    free(temp);           // 释放原栈顶
    return elem;
}
// 判断空栈
int isEmptyLinkStack(LinkStack* top) {
    return top == NULL;  // 栈空条件
}

6. 队列(Queue)- 顺序实现

结构体定义与特点

#include 
#include 
#define QUEUE_SIZE 100
typedef struct {
    int data[QUEUE_SIZE];  // 数组存储
    int front;             // 队头指针
    int rear;              // 队尾指针
} SeqQueue;

结构特点

FIFO(先进先出)                队尾插入,队头删除                存在"假溢出"问题

核心函数实现对比

// 初始化队列
void initQueue(SeqQueue* queue) {
    queue->front = 0;
    queue->rear = 0;  // 队空条件:front == rear
}
// 入队
int enQueue(SeqQueue* queue, int elem) {
    if (queue->rear >= QUEUE_SIZE) return 0;
    queue->data[queue->rear++] = elem;  // 队尾插入,rear后移
    return 1;
}
// 出队
int deQueue(SeqQueue* queue) {
    if (queue->front == queue->rear) return -1;
    return queue->data[queue->front++];  // 队头取出,front后移
}
// 判断队空
int isEmptyQueue(SeqQueue* queue) {
    return queue->front == queue->rear;  // 队空条件
}

7. 循环队列(Circular Queue)

结构体定义与特点

#include 
#include 
#define CQ_SIZE 5  // 实际可用CQ_SIZE-1
typedef struct {
    int data[CQ_SIZE];  // 循环数组
    int front;          // 队头指针
    int rear;           // 队尾指针
} CircularQueue;

结构特点

数组首尾相接                解决"假溢出"问题                队空和队满的判断条件特殊

核心函数实现对比

// 初始化循环队列
void initCQueue(CircularQueue* queue) {
    queue->front = 0;
    queue->rear = 0;  // 循环队列空条件
}
// 入队(循环队列特有)
int enCQueue(CircularQueue* queue, int elem) {
    if ((queue->rear + 1) % CQ_SIZE == queue->front) return 0;  // 队满
    queue->data[queue->rear] = elem;
    queue->rear = (queue->rear + 1) % CQ_SIZE;  // 循环后移
    return 1;
}
// 出队(循环队列特有)
int deCQueue(CircularQueue* queue) {
    if (queue->front == queue->rear) return -1;  // 队空
    int elem = queue->data[queue->front];
    queue->front = (queue->front + 1) % CQ_SIZE;  // 循环后移
    return elem;
}
// 判断队满(循环队列特有)
int isCQueueFull(CircularQueue* queue) {
    return (queue->rear + 1) % CQ_SIZE == queue->front;  // 队满条件
}

函数说明表

函数名头文件参数返回值参数示例示例含义
initCQueuestdio.h, stdlib.hqueue: 队列指针&queue初始化循环队列
enCQueuestdio.h, stdlib.hqueue: 队列指针
elem: 元素
成功1,失败0&queue, 3元素3入队
deCQueuestdio.h, stdlib.hqueue: 队列指针队头元素&queue队头出队
isCQueueFullstdio.h, stdlib.hqueue: 队列指针队满1,否0&queue判断队满

实际应用示例

浏览器历史记录(栈的应用)

#include 
#include 
#include 
#define MAX_HISTORY 10
typedef struct {
    char* pages[MAX_HISTORY];
    int top;
} BrowserHistory;
void initBrowser(BrowserHistory* browser) {
    browser->top = -1;
}
void visitPage(BrowserHistory* browser, const char* url) {
    if (browser->top >= MAX_HISTORY - 1) {
        free(browser->pages[0]);
        for (int i = 0; i < MAX_HISTORY - 1; i++) {
            browser->pages[i] = browser->pages[i + 1];
        }
        browser->top--;
    }
    browser->pages[++browser->top] = strdup(url);
}
char* goBack(BrowserHistory* browser) {
    if (browser->top <= 0) return NULL;
    return browser->pages[--browser->top];
}
char* goForward(BrowserHistory* browser) {
    if (browser->top >= MAX_HISTORY - 1) return NULL;
    return browser->pages[++browser->top];
}

数据结构对比总结

内存分配方式

数据结构存储方式内存连续性容量限制
顺序表静态数组连续固定大小
单链表动态节点不连续理论无限
双向链表动态节点不连续理论无限
顺序栈静态数组连续固定大小
链栈动态节点不连续理论无限
顺序队列静态数组连续固定大小
循环队列静态数组连续固定大小

操作复杂度对比

操作顺序表单链表双向链表队列
访问O(1)O(n)O(n)O(1)O(1)
插入O(n)O(1)O(1)O(1)O(1)
删除O(n)O(1)O(1)O(1)O(1)
查找O(n)O(n)O(n)O(n)O(n)

常见面试题

1. 基础概念题

Q1:顺序表和链表的区别是什么?

存储方式:顺序表连续,链表离散                访问效率:顺序表O(1),链表O(n)

插入删除:顺序表O(n),链表O(1)                空间效率:顺序表更优,链表有指针开销

Q2:栈和队列的主要区别?

栈:LIFO(后进先出),只能在栈顶操作

队列:FIFO(先进先出),队尾插入,队头删除

2. 算法实现题

Q3:用两个栈实现队列

typedef struct {
    SeqStack stack1;  // 用于入队
    SeqStack stack2;  // 用于出队
} StackQueue;
void enStackQueue(StackQueue* sq, int elem) {
    pushStack(&sq->stack1, elem);
}
int deStackQueue(StackQueue* sq) {
    if (isEmptyStack(&sq->stack2)) {
        while (!isEmptyStack(&sq->stack1)) {
            pushStack(&sq->stack2, popStack(&sq->stack1));
        }
    }
    return popStack(&sq->stack2);
}

Q4:判断链表是否有环

int hasCycle(ListNode* head) {
    if (head == NULL) return 0;
    ListNode* slow = head;
    ListNode* fast = head;
    while (fast != NULL && fast->next != NULL) {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast) return 1;
    }
    return 0;
}

3. 综合应用题

Q5:设计LRU缓存机制

使用哈希表+双向链表实现                哈希表提供O(1)访问                双向链表维护访问顺序

Q6:用队列实现栈

typedef struct {
    SeqQueue queue1;
    SeqQueue queue2;
} QueueStack;
void pushQueueStack(QueueStack* qs, int elem) {
    enQueue(&qs->queue1, elem);
}
int popQueueStack(QueueStack* qs) {
    while (qs->queue1.front != qs->queue1.rear - 1) {
        enQueue(&qs->queue2, deQueue(&qs->queue1));
    }
    int elem = deQueue(&qs->queue1);
    SeqQueue temp = qs->queue1;
    qs->queue1 = qs->queue2;
    qs->queue2 = temp;
    return elem;
}

性能优化建议

  1. 顺序结构:适合查询多、增删少的场景

  2. 链式结构:适合频繁插入删除的场景

  3. 栈的应用:函数调用、表达式求值、括号匹配

  4. 队列的应用:消息队列、任务调度、广度优先搜索

总结

通过对比七种线性数据结构的结构体定义和函数实现,我们可以清楚地看到:

  1. 顺序表适合随机访问,但插入删除效率低

  2. 链表适合频繁插入删除,但访问效率低

  3. 适合LIFO场景,操作受限但效率高

  4. 队列适合FIFO场景,解决顺序处理问题

  5. 循环队列解决了顺序队列的假溢出问题

理解这些数据结构的本质区别和适用场景,对于设计高效算法和解决实际问题具有重要意义。在实际开发中,大家根据具体需求选择合适的数据结构,平衡时间效率和空间效率。