数据结构:链表、栈、队列

目录

  • 链表、栈、队列
    • 链表
      • 空间使用的区别
      • 链表类型
        • 单链表节点定义
        • 单链表示例
        • 双链表节点定义
        • 双链表使用
    • 栈(Stack)LIFO
      • 栈定义
      • 栈的使用
      • 中缀表达式转后缀表达式(逆波兰表达式)
    • 队列(queue) FIFO
      • 队列的定义
      • 队列的使用

链表、栈、队列

计算机科学中的数据结构是算法设计的基础。本文将详细介绍链表、栈和队列这三种常见的数据结构,并重点分析链表的各种形式及其操作方法。

链表

链表是一种线性数据结构,与数组不同的是,它不需要连续的内存空间来存储数据。链表中的每个元素都是一个独立的节点,这些节点通过指针相互连接起来。

空间使用的区别

  • 线性表(数组实现):需要预先分配足够大的连续内存空间,以便容纳所有数据项。这种分配方式可能会导致内存浪费,特别是在不确定数据大小的情况下。
  • 链表:在需要添加元素时动态分配空间,因此空间不一定连续。这种方式更加灵活,能够更高效地利用内存资源。

链表类型

链表可以根据节点之间的连接方式分为以下几种:

  • 单链表:每个节点包含一个指向下一个节点的指针。
  • 双链表:每个节点包含两个指针,分别指向其前一个节点和后一个节点。
  • 循环链表:最后一个节点的下一个指针不是指向NULL,而是返回链表的第一个节点。
单链表节点定义
struct Node {   // 单向链表节点定义
    int data;   // 数据域,例如整数
    struct Node *next;  // 指向下一个节点的指针
};
单链表示例
/*
 * @Author: ZPY
 * @TODO: 单向链表的定义和使用
 */

#include 
#include 

// 节点
typedef struct node
{
    int data;
    struct node *next;
} Node;

// 两种定义链表的方式
// 1
typedef struct link
{
    struct node *head;
    int size;
} Link;

// 2
// typedef struct node *Link;

// 操作
void init(Link *link);
void add(Link *link, int value);
void display(Link *link);
void insert(Link *link, int index, int value);
void delete(Link *link, int index);
void clear(Link *link);
int find(Link *link, int value);
int size(Link *link);

int main(int argc, char const *argv[])
{
    // 创建
    Link *link = malloc(sizeof(Link));

    // 初始化
    init(link);

    // 添加元素
    add(link, 1);
    add(link, 2);
    add(link, 3);

    // 显示
    display(link);

    // 删除
    delete(link, 1);

    // 显示
    display(link);

    // 插入
    insert(link, 1, 4);

    // 显示
    display(link);

    return 0;
}

void init(Link *link)
{
    link->size = 0;
    link->head = malloc(sizeof(Node));
    link->head->next = NULL;
}

void add(Link *link, int value)
{
    // Node node = {value, NULL};   // 两种创建头节点的方式
    Node *node = malloc(sizeof(Node));
    node->data = value;
    node->next = NULL;

    if (link->head->next == NULL)   // 链表为空
    {
        link->head->next = node;
    }
    else
    {
        // 头插法
        // node->next = link->head->next;
        // link->head->next = node;

        // 尾插法
        Node *n = link->head;
        for (int i = 0; i < link->size; i++)
        {
            n = n->next;
        }
        n->next = node; // 找到最后一个节点后将新节点添加到链表的末尾
    }
    link->size++;
}

void display(Link *link)
{
    Node *node = link->head->next;

    while (node)
    {
        printf("data = %d, ", node->data);
        node = node->next;
    }
    printf("\n");
}

void insert(Link *link, int index, int value)
{
    if (index < 0 || index > link->size)
    {
        printf("请输入正确索引\n");
        return;
    }
    Node *pre = link->head;

    while (index--)
    {
        pre = pre->next;
    }
    Node *node = malloc(sizeof(Node));
    node->data = value;
    node->next = pre->next;
    pre->next = node;
    link->size++;

    free(node);
}

void delete(Link *link, int index)
{
    if (index < 0 || index >= link->size)
    {
        printf("请输入正确索引\n");
        return;
    }
    // 前驱节点
    Node *pre = link->head;
    
    for (int i = 0; i < index; i++)
    {
        pre = pre->next;
    }
    // 获取要删除的节点
    Node *node = pre->next;
    pre->next = node->next;
    link->size--;   // 大小减一
    free(node);     // 释放删除节点
}

int find(Link *link, int value)
{
    Node *node = link->head->next;
    int index = 0;

    while (node)
    {
        if (node->data == value)
        {
            return index;
        }
        node = node->next;
        index++;
    }

    return -1;
}

void clear(Link *link)
{
    Node *node = link->head;
    Node *f = node;

    while (node)
    {
        node = node->next;
        f = node;
        free(f);    // 遍历元素并释放空间
    }
    link->head->next = NULL;
    link->size = 0;
}

int size(Link *link)
{
    return link->size;
}
双链表节点定义
struct node     // 节点
{
    int data;           // 数据域
    struct node *prev;  // 前驱
    struct node *next;  // 后继
};
双链表使用
/*
 * @Author: ZPY
 * @TODO: 双向链表的定义和使用,双向链表使用尾插法更方便
 */

#include 
#include 

typedef struct node     // 节点
{
    int data;           // 数据域
    struct node *prev;  // 前驱
    struct node *next;  // 后继
} Node;

// typedef struct node Link;    // 一个指针

typedef struct link             // 两个指针
{
    Node *head;     // 头结点
    Node *tail;     // 尾结点
    int size;       // 链表长度
} Link;

// 操作
void init(Link *link);
void add(Link *link, int value);
void display(Link *link);
void insert(Link *link, int index, int value);
void delete(Link *link, int index);
void clear(Link *link);
int find(Link *link, int value);
int size(Link *link);
int get(Link *link, int index);

int main(int argc, char const *argv[])
{
    Link *link = (Link *)malloc(sizeof(Link));  // C++ 不允许隐式转换,因此在 C++ 中申请空间需要强转,而 C 则不用

    // 初始化
    init(link);

    // 添加
    add(link, 1);
    add(link, 2);
    add(link, 3);

    // 显示
    display(link);

    // 插入
    insert(link, 1, 4);

    // 显示
    display(link);

    return 0;
}

void init(Link *link)
{
    link->size = 0;
    link->head = malloc(sizeof(Node));
    link->tail = malloc(sizeof(Node));
    link->head->next = link->tail;
    link->tail->prev = link->head;
}

void add(Link *link, int value) // 时间复杂度 O(1)
{
    Node *node = malloc(sizeof(Node));
    node->data = value;

    link->tail->prev->next = node;  // 尾结点前驱的后继指向新节点
    node->prev = link->tail->prev;  // 新节点的前驱指向尾结点的前驱

    node->next = link->tail;    // 新节点的后继指向尾结点
    link->tail->prev = node;    // 尾结点的前驱指向新节点

    link->size++;
}

void display(Link *link)
{
    Node *node = link->head->next;
    for (int i = 0; i < link->size; i++)
    {
        printf("list[%d] = %d, ", i, node->data);
        node = node->next;
    }
    printf("\n");
}

void insert(Link *link, int index, int value)
{
    if (index < 0 || index > link->size)
    {
        printf("请输入正确索引\n");
        return;
    }
    Node *node = malloc(sizeof(Node));
    node->data = value;

    Node *n = link->head->next;
    while (index--)
    {
        n = n->next;
    }
    n->prev->next = node;   // 新节点的前驱指向插入位置的前一个节点
    node->prev = n->prev;   // 新节点的后继指向插入位置的节点

    node->next = n;         // 新节点的后继指向插入位置的节点
    n->prev = node;         // 插入位置的前一个节点的后继指向新节点

    link->size++;
}

void delete(Link *link, int index)
{
    if (index < 0 || index > link->size)
    {
        printf("请输入正确索引\n");
        return;
    }
    Node *node = link->head->next;
    while (index--)
    {
        node = node->next;
    }

    node->prev->next = node->next;  // 删除节点前驱的后继指向删除节点的后继
    node->next->prev = node->prev;  // 删除节点后继的前驱指向删除节点的前驱

    free(node); // 释放删除节点内存
    link->size--;
}

int find(Link *link, int value)
{
    Node *node = link->head->next;
    int index = 0;

    while (node != link->tail)
    {
        if (node->data == value)
        {
            return index;
        }
        node = node->next;
        index++;
    }

    return -1;
}

void clear(Link *link)
{
    Node *node = link->head;
    Node *f = node;

    while (node != link->tail)
    {
        node = node->next;
        f = node;
        free(f);    // 遍历元素并释放空间
    }
    link->head->next = link->tail;
    link->tail->prev = link->head;
    link->size = 0;
}

int size(Link *link)
{
    return link->size;
}

int get(Link *link, int index)
{
    Node *node = link->head->next;

    while (index--)
    {
        node = node->next;
    }

    return node->data;
}

栈(Stack)LIFO

概念:一种操作受限的表,只能从一端操作。LIFO:后进先出

操作:

  • push 压栈
  • pop 出栈
  • clear 清空

应用场景:

  • 浏览器前进、后退功能
  • 编译器、进程、函数的存储调用、
  • 语法检测、括号匹配
  • 中缀表达式转后缀表达式
  • 后缀表达式求值
  • 进制转换

实现:

  • 顺序栈(数组)
  • 链式栈(链表)

栈定义

struct stack
{
    int *data;      // 数组:在使用时若栈满需要扩容,链表则不需要
    int capacity;   // 容量
    int top;        // 栈顶
}

栈的使用

/*
 * @Author: ZPY
 * @TODO: 栈的定义和使用
 */

#include 
#include 

typedef struct stack
{
    int *data;      // 数组
    int capacity;   // 容量
    int top;        // 栈顶
} Stack;

// 栈操作
void init(Stack *s);        // 初始化
void push(Stack *s, int v); // 压栈
int pop(Stack *s);          // 出栈,删除栈顶元素并返回
int peek(Stack *s);         // 读取栈顶元素
int size(Stack *s);         // 栈大小
void increase(Stack *s);    // 扩容(数组需要,链表则无需)
int isEmpty(Stack *s);

int main(int argc, char const *argv[])
{
    Stack *s= malloc(sizeof(Stack));

    init(s);
    push(s, 1);
    push(s, 2);
    push(s, 3);
    printf("size = %d\n", size(s));

    printf("%d\n", pop(s));
    printf("%d\n", pop(s));
    printf("%d\n", pop(s));
    printf("size = %d\n", size(s));

    return 0;
}

void init(Stack *s)
{
    s->top = -1;
    s->capacity = 10;
    s->data = malloc(sizeof(int) * s->capacity);
}

void push(Stack *s, int v)
{
    if (s->top == s->capacity - 1)
    {
        increase(s);
    }

    s->top++;
    s->data[s->top] = v;
}

int pop(Stack *s)
{
    if (s->top == -1)
    {
        printf("栈空无法出栈\n");
        return -1;
    }

    return s->data[s->top--];
}

int peek(Stack *s)
{
    if (s->top == -1)
    {
        printf("栈空无法读取\n");
        return -1;
    }

    return s->data[s->top];
}

int size(Stack *s)
{
    return s->top + 1;
}

void increase(Stack *s)
{
    s->capacity << 1;   // 扩容为2倍
    s->data = realloc(s->data, sizeof(int) * s->capacity);
}

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

    return 0;
}

中缀表达式转后缀表达式(逆波兰表达式)

例如:1 + 2 * 3 - 4 / 2

中缀表达式存在缺陷:

  1. 运算符优先级
  2. 括号

方法:

  • 数组(队列):12, 3, 2, *, +, 9, 3, /, -
  • 运算符栈:

转换过程:

原中缀表达式 数组(后缀表达式) 运算符栈
1 + 2 * 3 - 4 / 2 NULL NULL
+ 2 * 3 - 4 / 2 1 +
2 * 3 - 4 / 2 1 +
* 3 - 4 / 2 1, 2 +
3 - 4 / 2 1, 2 +, *(乘法优先级高于加法,‘+’ 不出栈)
- 4 / 2 1, 2, 3 +, *
4 / 2 1, 2, 3, +, * -(减法优先级低于乘法且等于加法,栈内元素出栈两次)
/ 2 1, 2, 3, +, *, 4 -
2 1, 2, 3, +, *, 4 -, /(除法优先级高于减法,‘-’ 不出栈)
NULL 1, 2, 3, +, *, 4, 2 -, /
NULL 1, 2, 3, +, *, 4, 2, -, / NULL(中缀表达式遍历完成,栈内元素全部出栈)

运算符压栈操作:每次压栈前判断

  • +:优先级低于或等于栈顶元素,就将栈内元素出栈
  • -:
  • *:优先级高于栈顶元素,直接压栈
  • /:
  • (:直接压栈,无论栈内元素是什么
  • ):一直出栈,直到出栈一个‘(’,括号不用加入队列(后缀表达式)中

例:23 - (5 + 3 * 2) * 3 + 7

栈:

队列:23, 5, 3, 2, *, +, 3, *, -, 7, +

后缀表达式求值:

队列 数字栈 运算符栈
23, 5, 3, 2, *, +, 3, *, -, 7, + NULL NULL
+, 3, *, -, 7, + 23, 5, 3, 2 *(数字栈出栈两个元素进行运算后再压栈)
3, *, -, 7, + 23, 5, 6 +
-, 7, + 23, 11, 3 *
7, + 23, 33 -
NULL -10, 7 +
NULL -3 NULL

队列(queue) FIFO

概念:操作受限的表,两端操作,队列头出队,队列尾入队。FIFO,先入先出。

应用场景:

  • 网络请求存储在队列中
  • 事件处理 GUI 系统中 event 存入队列进行分发
  • Message Queue(MQ) 消息队列,同步机制,Kafka
  • 线程池

定义:

  • 数组
  • 链表

操作:

  • enqueue/add/put 入队
  • dequeue/poll 出队,轮询:每次出队一个元素
  • peek 读取队列中第一个元素
  • size 队列大小
  • clear 清空

队列的定义

struct Queue {
    int *arr;		// 数据
    int front;		// 队首
    int rear;		// 队尾
    int capacity;	// 容量
}

队列的使用

/*
 * @Author: ZPY
 * @TODO: 栈的定义和使用
 */
 
#include 
#include 

// 定义队列结构体
typedef struct Queue {
    int *arr;
    int front;
    int rear;
    int capacity;
} Queue;

// 初始化队列
Queue* initQueue(int capacity) {
    Queue* queue = (Queue*)malloc(sizeof(Queue));
    queue->capacity = capacity;
    queue->front = queue->rear = -1;
    queue->arr = (int*)malloc(queue->capacity * sizeof(int));
    return queue;
}

// 判断队列是否为空
int isEmpty(Queue* queue) {
    return queue->front == -1;
}

// 入队操作
void enqueue(Queue* queue, int data) {
    if (queue->rear == queue->capacity - 1) {
        printf("Queue Overflow\n");
        return;
    }
    queue->arr[++queue->rear] = data;
    if (queue->front == -1) { // 如果队列之前是空的
        queue->front = 0;
    }
}

// 出队操作
int dequeue(Queue* queue) {
    if (isEmpty(queue)) {
        printf("Queue Underflow\n");
        return INT_MIN;
    }
    int data = queue->arr[queue->front];
    queue->front++;
    if (queue->front > queue->rear) { // 如果队列变为空
        queue->front = queue->rear = -1;
    }
    return data;
}

// 获取队列大小
int size(Queue* queue) {
    if (isEmpty(queue)) {
        return 0;
    }
    return (queue->rear - queue->front + 1);
}

int main() {
    Queue* q = initQueue(5);

    enqueue(q, 1);
    enqueue(q, 2);
    enqueue(q, 3);
    printf("Size = %d\n", size(q));

    printf("%d\n", dequeue(q));
    printf("%d\n", dequeue(q));
    printf("%d\n", dequeue(q));
    printf("Size = %d\n", size(q));

    return 0;
}

你可能感兴趣的:(数据结构,链表,c语言,ubuntu,vscode)