数据结构(郝斌)

目录

数据结构的定义

常用函数或定义

strcpy

malloc

free

typedef

define

算法

排序

冒泡排序(Bubble Sort)

插入排序(Insertion Sort) 

选择排序(Selection Sort)

快速排序(Quick Sort)

归并排序(Merge Sort)

 指针

结构体(C++中用类也能实现)

 为什么会出现结构体 ?

什么叫结构体?

如何使用结构体

模块一:线性结构【把所有的结点用一根直线穿起来】

连续存储【数组】

        1、什么叫做数组

        2、数组的优缺点:

      3.算法   

增加/插入:在数组的某个位置插入新元素,需要将插入位置之后的元素向右移动。

删除

   倒置

选择排序

冒泡排序 

   离散存储【链表】(我们搞底层的开发,类似于SUN公司的类)

定义:

 专业术语:

分类: 

链表的优缺点:

 算法

1.声明

插入

删除

 创建

遍历

 排序

线性结构的两种常见应用之一   栈   (存储数据的结构)

定义:

 分类

算法(往里放,从里取)

1. 头文件和数据结构定义

2. 初始化栈

 3. 入栈操作

 4. 判断栈是否为空

 5. 遍历栈并打印元素

 6. 出栈操作

 7. 清空栈

应用

 线性结构的两种常见应用之二   队列

定义:

 分类:

​编辑

循环队列的讲解:

 1、 静态队列为什么必须是循环队列

 2、 循环队列需要几个参数来确定 及其含义

3、 循环队列各个参数的含义

  4、   循环队列入队伪算法讲解

5、 循环队列出队伪算法讲解

6、 如何判断循环队列是否为空

7、 如何判断循环队列是否已满

​编辑

 队列算法:

1. 数据结构声明 

2. 初始化 

3. 判断队列是否满

4. 判断队列是否空

5. 入队

6. 出队

7. 遍历

8. 应用

 专题:递归

定义:

循环和递归:

举例:   

模块二:非线性结构

   树

 树定义

专业术语             

分类

树的存储(都是转化成二叉树来存储)

代码

1. 结构体声明和函数声明

2. 主函数 

3. 创建二叉树

4. 前序遍历

 5. 中序遍历 

6. 后序遍历


数据结构的定义

       我们如何把现实中大量而复杂的问题以特定的数据类型(单 个数据怎样存储?)和特定的存储结构(个体的关系) 保存到主存储器(内存)中,以及在此基础上为实现某个功能(比如查找某个元素,删除某个元素,对所有元素进行排序)而执行的相应操作,这个相应的操作也叫算法。(比如班里有15个人,其信息量也许一个数组就搞定了,但是假如10000个, 怎么办?内存也许没有这么多连续的空间,所以我们改用链表,you see这就是与存储有关系。又比如,人事管理系统的信息存储,因为存在着上下级的关系,所以数组和链表就无能为力了,这时候我们用树,再比如我们做的是交通图,站和站之间肯定要连通,这时候以上的存储方式又无能为力了,所以我们又有了图。图 就是每个结点都可以和其他结点产生联系。所以当我们要解决 问题时,首先要解决的是如何把这些问题转换成数据,先保存 到我们的主存中)

数据结构 = 个体的存储 + 个体的关系的存储

算法 = 对存储数据的操作

常用函数或定义

strcpy


    strcpy 是 C 语言标准库函数之一,用于字符串复制。它将源字符串的内容复制到目标字符串中,包括终止的空字符 \0。函数原型定义在 <string.h> 头文件中。
eg:
char dest[20]; // 目的字符串,需要有足够的空间

const char src[] = "Hello, World!"; // 源字符串

    strcpy(dest, src); // 复制字符串

    printf("复制后的字符串: %s\n", dest);


malloc


    在 C 语言中,动态内存分配是一种在程序运行时分配内存的方法。函数定义在 <stdlib.h> 头文件中。
malloc():分配指定大小的内存块。它返回一个指向 void 类型的指针,因此需要强制类型转换到所需的类型。
int *p = (int*)malloc(10 * sizeof(int)); // 分配10个整数的空间
if (p == NULL) {
    // 处理分配失败的情况
}


free


    free():释放之前分配的内存。这是非常重要的,因为动态分配的内存不会在作用域结束或函数返回时自动释放。
free(p); // 释放p指向的内存
p = NULL; // 将p设置为NULL,避免悬空指针


typedef


    // 使用 typedef 为 int 创建新的类型名 Integer
typedef int Integer;


define


    #define 是预处理指令,用于定义宏,可以是常量、表达式、代码片段等。

算法

狭义的算法是与数据的存储方式密切相关

 广义的算法是与数据的存储方式无关

      泛型:(给你一种假象,只不过牛人从内部都弄好了)  利用某种技术达到的效果就是:不同的存储方式,执行的操作是一样的

算法的真正学法:很多算法你根本解决不了!!!!!!因为很多都属于  数学上的东西,所以我们把答案找出来,如果能看懂就 行,但是大部分人又看不懂,分三步,按照流程,语句,试数。这个过程肯定会不断地出错,所以不断出错,不断改错,这样反复敲很多次,才能有个提高。实在看不懂 就先背会。     

  

排序

冒泡排序(Bubble Sort)

#include <stdio.h>

// 冒泡排序函数
void bubble_sort(int arr[], int len) {
    int i, j, temp;
    for (i = 0; i < len - 1; i++) // 外层循环控制排序的趟数
        for (j = 0; j < len - 1 - i; j++) // 内层循环进行相邻元素比较
            if (arr[j] > arr[j + 1]) { // 比较相邻元素
                temp = arr[j]; // 交换
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
}

// 主函数
int main() {
    int arr[] = { 22, 34, 3, 32, 82, 55, 89, 50, 37, 5, 64, 35, 9, 70 };
    int len = sizeof(arr) / sizeof(arr[0]);
    bubble_sort(arr, len);
    int i;
    for (i = 0; i < len; i++)
        printf("%d ", arr[i]);
    return 0;
}

/* 图解:
   初始数组: 22 34 3 32 82 55 89 50 37 5 64 35 9 70
   第一趟排序后:
   22 3 32 34 55 82 50 37 5 64 35 9 70 89
   第二趟排序后:
   3 22 32 34 55 50 37 5 64 35 9 70 82 89
   ...
   最终排序完成:
   3 5 9 22 32 34 35 37 50 55 64 70 82 89
*/

插入排序(Insertion Sort) 

#include <stdio.h>

// 交换两个变量的函数
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

// 选择排序函数
void selection_sort(int arr[], int len) {
    int i, j;
    for (i = 0; i < len - 1; i++) {
        int min = i;
        for (j = i + 1; j < len; j++) // 遍历未排序的部分
            if (arr[j] < arr[min]) // 找到最小值
                min = j; // 记录最小值的位置
        swap(&arr[min], &arr[i]); // 交换
    }
}

// 主函数
int main() {
    int arr[] = { 22, 34, 3, 32, 82, 55, 89, 50, 37, 5, 64, 35, 9, 70 };
    int len = sizeof(arr) / sizeof(arr[0]);
    selection_sort(arr, len);
    int i;
    for (i = 0; i < len; i++)
        printf("%d ", arr[i]);
    return 0;
}

/* 图解:
   初始数组: 22 34 3 32 82 55 89 50 37 5 64 35 9 70
   第一轮排序后:
   3 22 34 32 82 55 89 50 37 5 64 35 9 70
   第二轮排序后:
   3 5 22 32 82 55 89 50 37 34 64 35 9 70
   ...
   最终排序完成:
   3 5 9 22 32 34 35 37 50 55 64 70 82 89
*/

选择排序(Selection Sort)

#include <stdio.h>

// 交换两个变量的函数
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

// 选择排序函数
void selection_sort(int arr[], int len) {
    int i, j;
    for (i = 0; i < len - 1; i++) {
        int min = i;
        for (j = i + 1; j < len; j++) // 遍历未排序的部分
            if (arr[j] < arr[min]) // 找到最小值
                min = j; // 记录最小值的位置
        swap(&arr[min], &arr[i]); // 交换
    }
}

// 主函数
int main() {
    int arr[] = { 22, 34, 3, 32, 82, 55, 89, 50, 37, 5, 64, 35, 9, 70 };
    int len = sizeof(arr) / sizeof(arr[0]);
    selection_sort(arr, len);
    int i;
    for (i = 0; i < len; i++)
        printf("%d ", arr[i]);
    return 0;
}

/* 图解:
   初始数组: 22 34 3 32 82 55 89 50 37 5 64 35 9 70
   第一轮排序后:
   3 22 34 32 82 55 89 50 37 5 64 35 9 70
   第二轮排序后:
   3 5 22 32 82 55 89 50 37 34 64 35 9 70
   ...
   最终排序完成:
   3 5 9 22 32 34 35 37 50 55 64 70 82 89
*/

快速排序(Quick Sort)

#include <stdio.h>

// 快速排序函数声明
void QuickSort(int* a, int low, int high);
int FindPos(int* a, int low, int high);

// 主函数
void main() {
    int a[6] = { 2, 1, 0, 5, 4, 3 };
    int i;
    QuickSort(a, 0, 5);
    for (i = 0; i < sizeof(a) / sizeof(a[0]); i++) {
        printf("%d\n", a[i]);
    }
    return;
}

// 快速排序函数实现
void QuickSort(int* a, int low, int high) {
    int pos;
    if (low < high) {
        pos = FindPos(a, low, high);
        QuickSort(a, low, pos - 1);
        QuickSort(a, pos + 1, high);
    }
}

// 找到分割位置
int FindPos(int* a, int low, int high) {
    int val = a[low]; // 选定基准值
    while (low < high) {
        while (low < high && a[high] >= val)
            --high;
        a[low] = a[high];
        while (low < high && a[low] <= val)
            ++low;
        a[high] = a[low];
    }
    a[low] = val;
    return low;
}

/* 图解:
   初始数组: 2 1 0 5 4 3
   第一次划分后:
   1 0 2 5 4 3
   第二次划分后:
   0 1 2 5 4 3
   第三次划分后:
   0 1 2 3 4 5
   ...
   最终排序完成:
   0 1 2 3 4 5
*/

归并排序(Merge Sort)

#include <stdio.h>
#include <stdlib.h>

// 归并两个子数组
void merge(int arr[], int l, int m, int r) {
    int n1 = m - l + 1;
    int n2 = r - m;
    int* L = (int*)malloc(n1 * sizeof(int));
    int* R = (int*)malloc(n2 * sizeof(int));
    
    // 复制数据到临时数组
    for (int i = 0; i < n1; i++)
        L[i] = arr[l + i];
    for (int j = 0; j < n2; j++)
        R[j] = arr[m + 1 + j];

    // 合并临时数组回原数组
    int i = 0, j = 0, k = l;
    while (i < n1 && j < n2) {
        if (L[i] <= R[j]) {
            arr[k] = L[i];
            i++;
        } else {
            arr[k] = R[j];
            j++;
        }
        k++;
    }

    // 复制剩余元素
    while (i < n1) {
        arr[k] = L[i];
        i++;
        k++;
    }
    while (j < n2) {
        arr[k] = R[j];
        j++;
        k++;
    }
    
    free(L);
    free(R);
}

// 归并排序函数
void merge_sort(int arr[], int l, int r) {
    if (l < r) {
        int m = l +

 指针

        指针的重要性:(内存是可以被CPU直接访问的,硬盘不行 主要靠地址总线,数据总线,控制总线。)指针是C语言的灵魂

int **p; // 声明一个指向指针的指针

int *p;
fun(&p);
int fun(int  **p){};

       定义

地址: 地址就是内存单元的编号

             从0开始的非负整数

             范围:0--FFFFFFFF[0-4G-1](地址线是32位,刚好控制2的32次)

  指针:

                指针就是地址  地址就是指针

                指针变量是存放内存单元地址的变量

                指针的本质是一个操作受限的非负整数(不能加乘除,只能减)

分类:

            1、基本类型的指针

            2、指针和数组的关系

结构体(C++中用类也能实现)

 为什么会出现结构体 ?

为了表示一些复杂的数据,而普通的基本类型变量无法满足要求

什么叫结构体?

结构体是用户根据实际需要自己定义的复合数据类型

如何使用结构体

 两种方式:        

如何使用结构体   两种方式:
    struct Student st = {int 1000, string "zhangsan", int 20}
    struct Student * pst = &st;  			
    1.st.sid
    2.pst->sid
   pst所指向的结构体变量中的sid这个成员
void test1(Struct Student  st){}//传递的是一大串地址208个字节(消耗时间)
void test2(Struct Student  *st){}//传递的是4个字节

    注意事项: 结构体变量不能加减乘除,但可以相互赋值

     普通结构体变量和结构体指针变量作为函数参数的传递       

// 定义节点结构体
typedef struct Node {
    int data;           // 数据域
    struct Node* next;  // 指向下一个节点的指针
} Node,*pNode;

模块一:线性结构【把所有的结点用一根直线穿起来】

连续存储【数组】

        1、什么叫做数组

            元素类型相同,大小相等(数组传参,只要传进去首地址和长度就行)

        2、数组的优缺点:

            优点: 存取速度快

            缺点:事先必须知道数组的长度

                插入删除元素很慢

                空间通常是有限制的

                需要大块连续的内存块

                插入删除元素的效率很低

      3.算法   

增加/插入:在数组的某个位置插入新元素,需要将插入位置之后的元素向右移动。
// 在指定位置插入元素
// arr: 要操作的数组
// pos: 插入的位置
// val: 要插入的值
// n: 当前数组的长度(元素个数)

// 首先,将插入位置之后的元素向右移动一位
for (int i = n; i > pos; i--) {
    arr[i] = arr[i - 1]; // 将 arr[i-1] 的值复制到 arr[i]
}

// 将新值插入到指定位置
arr[pos] = val; // 将 val 插入到 arr[pos]

// 更新数组的长度
n++; // 数组的元素个数增加1
删除
// 从数组中删除指定位置的元素
// arr: 要操作的数组
// pos: 要删除的元素位置
// n: 当前数组的长度(元素个数)

// 将删除位置之后的元素向左移动一位
for (int i = pos; i < n - 1; i++) {
    arr[i] = arr[i + 1]; // 将 arr[i+1] 的值复制到 arr[i]
}

// 更新数组的长度
n--; // 数组的元素个数减少1
   倒置
// 将数组中的元素反向排列
// arr: 要操作的数组
// n: 数组的长度(元素个数)

// 遍历数组的一半
for (int i = 0; i < n / 2; i++) {
    // 交换当前元素和其对称位置的元素
    int temp = arr[i]; // 临时存储当前元素的值
    arr[i] = arr[n - i - 1]; // 将对称位置的元素复制到当前位置
    arr[n - i - 1] = temp; // 将临时存储的值复制到对称位置
}
选择排序
// 对数组进行选择排序
// arr: 要排序的数组
// n: 数组的长度(元素个数)

// 遍历数组中的每一个元素
for (int i = 0; i < n - 1; i++) {
    int minIndex = i; // 假设当前元素为最小值

    // 找到从当前元素开始的未排序部分的最小值
    for (int j = i + 1; j < n; j++) {
        if (arr[j] < arr[minIndex]) { // 如果发现更小的值
            minIndex = j; // 更新最小值的索引
        }
    }

    // 交换当前元素和找到的最小值
    int temp = arr[i]; // 临时存储当前元素的值
    arr[i] = arr[minIndex]; // 将最小值赋值给当前元素
    arr[minIndex] = temp; // 将临时存储的值赋值给最小值位置
}
冒泡排序 
// 对数组进行冒泡排序
// arr: 要排序的数组
// n: 数组的长度(元素个数)

// 外层循环控制排序的轮数
for (int i = 0; i < n - 1; i++) {
    // 内层循环进行相邻元素的比较和交换
    for (int j = 0; j < n - i - 1; j++) {
        if (arr[j] > arr[j + 1]) { // 如果当前元素大于下一个元素
            // 交换两个元素的位置
            int temp = arr[j]; // 临时存储当前元素的值
            arr[j] = arr[j + 1]; // 将下一个元素

   离散存储【链表】(我们搞底层的开发,类似于SUN公司的类)

定义:

          n个节点离散分配

            彼此通过指针相连

            每个节点只有一个前驱节点,每个节点只有一个后续节点

            首节点没有前驱节点,尾节点没有后续节点。

 专业术语:

首节点: 第一个有效节点

 尾节点:   最后一个有效节点

 头节点:头结点的数据类型和首节点的类型一样没有存放有效数据,最最前面的,是在首节点之前的,主要是为了方便对链表的操作。

 头指针:(指向头)   指向头节点的指针变量----phead

 尾指针:指向尾节点的指针

(头结点有可能很大,占的内存可能大,假设我想造一个函数输出所有链表的值,那你如果不用头指针类型做形参,那由于不同链表的头节点不一样大小,这样就没办法找出形参)

确定一个链表需要几个参数:(或者说如果期望一个函数对链表进行操作

   我们至少需要接收链表的那些信息???)

 只需要一个参数:头指针,因为通过它我们可以推出 链表的所有信息

(链表的程序最好一定要自己敲出来)

分类: 

单链表

双链表 :每一个节点有两个指针域

循环链表:能通过任何一个节点找到其他所有的节点

非循环链表

链表的优缺点:

优点:  空间没有限制

              插入删除元素很快

  缺点:  存取速度很慢。

 算法

(java中变成垃圾内存则会自动释放,但是C和C++则不会,所以要

手动释放,否则会引起内存泄露。delete等于free)   

1.声明

// 链表节点的结构体声明
// data: 存储节点数据的整型成员
// pNext: 指向下一个节点的指针
typedef struct Node {
    int data; // 节点数据
    struct Node* pNext; // 指向下一个节点的指针
} NODE, *PNODE; // NODE 是结构体类型的别名,PNODE 是指向 NODE 的指针类型
插入

方法一: 使用临时变量插入
// 在节点 p 后插入新节点 q
// p: 插入位置之前的节点
// q: 要插入的新节点

void insertAfter(Node* p, Node* q) {
    Node* r = p->pNext; // 临时变量 r 指向 p 的下一个节点
    p->pNext = q; // 将 p 的 pNext 指针指向新节点 q
    q->pNext = r; // 将新节点 q 的 pNext 指针指向原来的下一个节点
}

// 图解:
// 插入前:
// p -> [A] -> [B] -> [C] -> NULL
//
// 插入后 (插入节点 q):
// p -> [A] -> [q] -> [B] -> [C] -> NULL

方法二: 直接插入

// 在节点 p 后插入新节点 q
// p: 插入位置之前的节点
// q: 要插入的新节点

void insertAfter(Node* p, Node* q) {
    q->pNext = p->pNext; // 将新节点 q 的 pNext 指针指向 p 的原下一个节点
    p->pNext = q; // 将 p 的 pNext 指针指向新节点 q
}

// 图解:
// 插入前:
// p -> [A] -> [B] -> [C] -> NULL
//
// 插入后 (插入节点 q):
// p -> [A] -> [q] -> [B] -> [C] -> NULL
删除
// 删除节点 p 后的节点,并释放内存
// p: 要删除的节点前一个节点的指针
// 删除节点 p->pNext 指向的节点

void deleteAfter(Node* p) {
    Node* q = p->pNext; // 临时变量 q 存放要删除的节点
    if (q != NULL) { // 如果 q 不为空
        p->pNext = q->pNext; // 将 p 的 pNext 指针指向 q 的下一个节点
        free(q); // 释放要删除节点的内存
        q = NULL; // 设置 q 为 NULL
    }
}

// 图解:
// 删除前:
// p -> [A] -> [B] -> [C] -> NULL
//
// 删除后 (删除节点 B):
// p -> [A] -> [C] -> NULL
 创建
// 创建链表的函数实现
// 返回链表的头指针

PNODE create_list() {
    int len; // 存储用户输入的节点个数
    int i; // 循环控制变量
    int val; // 临时存储用户输入的节点值

    printf("请输入你要生成的节点个数:len= ");
    scanf_s("%d", &len); // 用户输入节点个数
    if (len <= 0) {
        printf("节点个数必须大于0。\n");
        return NULL; // 如果节点个数不合法,返回NULL
    }

    PNODE pHead = (PNODE)malloc(sizeof(NODE)); // 分配头节点内存
    if (pHead == NULL) {
        printf("分配失败!\n");
        exit(-1); // 如果内存分配失败,程序退出
    }
    pHead->pNext = NULL; // 头节点的 pNext 指针初始化为 NULL

    PNODE pTail = pHead; // pTail 指针指向链表的尾部,初始时指向头节点

    for (i = 0; i < len; ++i) { // 循环创建链表的节点
        printf("请输入第%d个节点的值: ", i + 1);
        scanf_s("%d", &val); // 用户输入节点值
        PNODE pNew = (PNODE)malloc(sizeof(NODE)); // 分配新节点内存
        if (pNew == NULL) {
            printf("分配失败!\n");
            exit(-1); // 如果内存分配失败,程序退出
        }
        pNew->data = val; // 将用户输入的值赋给新节点的数据域
        pNew->pNext = NULL; // 新节点的 pNext 指针初始化为 NULL

        pTail->pNext = pNew; // 将新节点链接到链表的尾部
        pTail = pNew; // 将 pTail 指针移动到新的尾部节点
    }

    return pHead; // 返回链表头指针
}

// 图解:
// 输入 3 个节点值: 10, 20, 30
//
// 链表:
// 头节点 -> [10] -> [20] -> [30] -> NULL
遍历
// 遍历链表并输出节点值
// head: 指向链表头节点的指针

void traverse_list(PNODE head) {
    if (head == NULL) { // 如果链表为空,输出提示并返回
        printf("链表为空!\n");
        return;
    }

    PNODE p = head->pNext; // p 指针指向链表的第一个实际节点
    while (p != NULL) { // 当 p 指针不为 NULL 时,循环继续
        printf("%d ", p->data); // 输出当前节点的数据域
        p = p->pNext; // 将 p 指针移动到下一个节点
    }
    printf("\n"); // 遍历完成后输出换行符
}

// 图解:
// 遍历链表:
// 链表: [10] -> [20] -> [30] -> NULL
// 输出: 10 20 30
 排序
// 对链表进行排序(选择排序)
// head: 指向链表头节点的指针
// len: 链表的长度(节点个数)

void sort_list(PNODE head, int len) {
    int i, j, t;
    PNODE p, q;

    for (i = 0, p = head->pNext; i < len - 1; ++i, p = p->pNext) {
        for (j = i + 1, q = p->pNext; j < len; ++j, q = q->pNext) {
            if (p->data > q->data) { // 如果当前节点的值大于下一个节点的值
                t = p->data; // 交换值
                p->data = q->data;
                q->data = t;
            }
        }
    }
}

// 图解:
// 排序前:
// 链表: [30] -> [10] -> [20] -> NULL
//
// 排序后:
// 链表: [10] -> [20] -> [30] -> NULL

线性结构的两种常见应用之一   栈   (存储数据的结构)

定义:

 一种可以实现“先进后出” 的存储结构  栈类似于箱子

堆(Heap)
内存分配和管理:堆是动态内存分配的区域,由程序员手动分配和清理(例如,使用new和delete在C++中,或者malloc和free在C中)。
大小限制:堆的大小通常远大于栈,可以达到GB级别,受限于系统的虚拟内存。
访问速度:堆的访问速度通常比栈慢,因为它可能不在处理器的缓存中。
存储内容:对象本身和数组通常存储在堆上。
生命周期:堆上的对象的生命周期是由分配和清理决定的,可以跨越多个函数调用和作用域

 分类

            静态栈 (类似于用数组实现) 静态栈在编译时分配固定大小的内存

            动态栈 (类似于用链表实现)动态栈在运行时根据需要动态分配内存

算法(往里放,从里取)

            出栈                            压栈(参看Java中线程的例子,成产消费的例子)

1. 头文件和数据结构定义
#include <stdio.h>
#include <stdlib.h> // 使用标准库的 malloc 和 free 函数

// 定义链表节点结构体
typedef struct Node {
    int data; // 存储节点数据
    struct Node* pNext; // 指向下一个节点的指针
} NODE, *PNODE;

// 定义栈结构体
typedef struct Stack {
    PNODE pTop; // 指向栈顶的指针
    PNODE pButton; // 指向栈底的指针(也作为空栈的标志)
} STACK, *PSTACK;
2. 初始化栈
// 初始化栈
void init(PSTACK pS) {
    // 为栈底节点分配内存
    pS->pTop = (PNODE)malloc(sizeof(NODE));
    if (pS->pTop == NULL) { // 检查内存分配是否成功
        printf("内存分配失败!\n");
        exit(-1); // 内存分配失败,退出程序
    } else {
        // 设置栈底指针
        pS->pButton = pS->pTop; // 栈底指针也指向这个节点
        pS->pButton->pNext = NULL; // 设置栈底节点的 pNext 为 NULL
        
        /* 图解
        +-----+   +-----+
        |  .  |   |  .  | <- pButton (栈底指针) 和 pTop (栈顶指针)
        +-----+   +-----+
        | NULL|   | NULL| <- pNext (指向下一个节点的指针)
        +-----+   +-----+
        */
    }
}
 3. 入栈操作
// 入栈操作
void push(PSTACK pS, int val) {
    // 创建一个新的节点
    PNODE pNew = (PNODE)malloc(sizeof(NODE));
    if (pNew == NULL) { // 检查内存分配是否成功
        printf("内存分配失败!\n");
        exit(-1); // 内存分配失败,退出程序
    }
    pNew->data = val; // 设置新节点的数据
    pNew->pNext = pS->pTop; // 将新节点的 pNext 指向当前栈顶
    pS->pTop = pNew; // 更新栈顶指针,指向新节点
    
    /* 图解
    新节点:
    +-----+
    |  5  | <- 新节点的 data
    +-----+
    |  *  | <- 新节点的 pNext 指向原栈顶
    +-----+
    
    入栈后:
    +-----+   +-----+
    |  5  |   |  .  | <- pTop (栈顶指针,指向新节点 5)
    +-----+   +-----+
    |  *  |   | NULL| <- pNext (新节点的 pNext 指向原栈顶)
    +-----+   +-----+
    */
}
 4. 判断栈是否为空
// 判断栈是否为空
_Bool empty(PSTACK pS) {
    // 如果栈顶指针和栈底指针相同,则栈为空
    return pS->pButton == pS->pTop;
    
    /* 图解
    如果栈为空,pTop 和 pButton 指向相同节点:
    +-----+
    |  .  | <- pTop (栈顶指针) 和 pButton (栈底指针)
    +-----+
    | NULL| <- pNext (指向下一个节点的指针)
    +-----+
    */
}
 5. 遍历栈并打印元素
// 遍历栈并打印元素
void traverse(PSTACK pS) {
    // 从栈顶开始遍历
    PNODE p = pS->pTop; // p 指向栈顶
    while (p != pS->pButton) { // 遍历直到栈底
        printf("%d  ", p->data); // 打印当前节点的数据
        p = p->pNext; // 移动到下一个节点
    }
    printf("\n"); // 打印换行符
    
    /* 图解
    假设栈中有节点 10, 20, 30:
    +-----+   +-----+   +-----+
    | 30  |   | 20  |   | 10  | <- pTop (栈顶指针)
    +-----+   +-----+   +-----+
    | *   |   | *   |   | NULL| <- pNext (指向下一个节点的指针)
    +-----+   +-----+   +-----+
    
    遍历顺序:30 -> 20 -> 10
    */
}
 6. 出栈操作
// 出栈操作
// 从栈中弹出元素,并将其值存入 pVal
// 如果出栈成功,返回 true;否则返回 false
_Bool pop(PSTACK pS, int* pVal) {
    if (empty(pS)) { // 如果栈为空
        return 0; // 返回 false
    } else {
        PNODE r = pS->pTop; // 临时变量 r 存放栈顶节点
        *pVal = r->data; // 将栈顶节点的数据存入 pVal
        pS->pTop = r->pNext; // 更新栈顶指针,指向下一个节点
        free(r); // 释放原栈顶节点的内存
        r = NULL; // 将 r 置为 NULL
        return 1; // 返回 true
    }
    
    /* 图解
    出栈操作:
    +-----+   +-----+
    | 30  |   | 20  | <- pTop (栈顶指针,指向新栈顶)
    +-----+   +-----+
    | *   |   | *   | <- pNext (更新后的指针)
    +-----+   +-----+
    
    被出栈的节点 30 被释放,pTop 指向 20。
    */
}
 7. 清空栈
// 清空栈的所有元素
void clear(PSTACK pS) {
    if (empty(pS)) { // 如果栈为空
        return; // 无需操作
    } else {
        PNODE p = pS->pTop; // p 指向栈顶
        PNODE q = NULL; // q 用于保存下一个节点
        while (p != pS->pButton) { // 遍历栈,直到栈底
            q = p->pNext; // 保存下一个节点
            free(p); // 释放当前节点
            p = q; // 移动到下一个节点
        }
        pS->pTop = pS->pButton; // 清空栈后,将栈顶指针指向栈底
    }
    
    /* 图解
    清空操作:
    +-----+   +-----+
    |  .  |   |  .  | <- pTop (更新后的栈顶指针)
    +-----+   +-----+
    | NULL|   | NULL| <- pNext (更新后的指针)
    +-----+   +-----+
    
    栈中所有节点被释放,pTop 和 pButton 指向同一节点。
    */
}
应用

            函数调用

            中断

            表达式求值(用两个栈,一个存放数字,一个存放符号)

            内存分配

            缓冲处理

            迷宫

       

 线性结构的两种常见应用之二   队列

定义:

一种可是实现“先进先出”的存储结构

 分类:

 链式队列:用链表实现

 静态队列:用数组实现   静态对流通常都必须是循环队列,为了减少内存浪费。

循环队列的讲解:

 1、 静态队列为什么必须是循环队列
 2、 循环队列需要几个参数来确定 及其含义

   需要2个参数来确定  front  rear

3、 循环队列各个参数的含义

2个参数不同场合不同的含义?     建议初学者先记住,然后慢慢体会

1)队列初始化     front和rear的值都是零

 2)队列非空       front代表队列的第一个元素  rear代表了最后一个有效元素的下一个元素

 3)队列空           front和rear的值相等,但是不一定是零

  4、   循环队列入队伪算法讲解

两步完成:

1)将值存入r所代表的位置

2)将r后移,正确写法是 rear = (rear+1)%数组长度

 错误写法:rear=rear+1;

5、 循环队列出队伪算法讲解

front = (front+1) % 数组长度

6、 如何判断循环队列是否为空

 如果front与rear的值相等,则队列一定为空

7、 如何判断循环队列是否已满

 预备知识:

 front的值和rear的值没有规律, 即可以大,小,等。

两种方式:

 1、多增加一个表标识的参数

2、少用一个队列中的元素(才一个,不影响的)

通常使用第二种方法如果r和f的值紧挨着,则队列已满用C语言伪算法表示就是:

if( (r+1)%数组长度 == f )

        已满

else

        不满           

 队列算法:

1. 数据结构声明 
// 队列结构体声明
// pBase: 存储队列数据的数组
// front: 指向队首的索引
// rear: 指向队尾的索引

typedef struct Quene {
    int* pBase; // 存储队列元素的数组
    int front; // 队首索引
    int rear;  // 队尾索引
} QUENE;
2. 初始化 
// 初始化队列
// 为队列分配内存并设置队首和队尾索引为0

void init(QUENE* pQ) {
    pQ->pBase = (int*)malloc(sizeof(int) * 6); // 分配内存,初始化数组大小为6
    pQ->front = 0; // 队首索引初始化为0
    pQ->rear = 0;  // 队尾索引初始化为0
}

// 图解:
// 队列初始化后:
// front = 0
// rear = 0
// 队列存储: [NULL, NULL, NULL, NULL, NULL, NULL] (大小为6)
3. 判断队列是否满
// 判断队列是否满
// 如果 rear + 1 与 front 相同,则队列满

_Bool full_quene(QUENE* pQ) {
    if ((pQ->rear + 1) % 6 == pQ->front) {
        return 1; // 队列满
    } else {
        return 0; // 队列未满
    }
}

// 图解:
// 队列满的条件:
// 如果队列的前一个位置 (rear + 1) 与队首索引 (front) 相等,则队列满。
// 队列存储: [a, b, c, d, e, x]  (rear = 5, front = 0)
4. 判断队列是否空
// 判断队列是否满
// 如果 rear + 1 与 front 相同,则队列满

_Bool full_quene(QUENE* pQ) {
    if ((pQ->rear + 1) % 6 == pQ->front) {
        return 1; // 队列满
    } else {
        return 0; // 队列未满
    }
}

// 图解:
// 队列满的条件:
// 如果队列的前一个位置 (rear + 1) 与队首索引 (front) 相等,则队列满。
// 队列存储: [a, b, c, d, e, x]  (rear = 5, front = 0)
5. 入队
// 判断队列是否空
// 如果 front 和 rear 相同,则队列空

_Bool empty_quene(QUENE* pQ) {
    if (pQ->front == pQ->rear) {
        return 1; // 队列空
    } else {
        return 0; // 队列未空
    }
}

// 图解:
// 队列空的条件:
// 如果队首索引 (front) 与队尾索引 (rear) 相等,则队列空。
// 队列存储: [a, b, c, d, e, f]  (rear = 0, front = 0)
6. 出队
// 出队操作
// 从队列中移除一个元素并更新队首索引

_Bool out_quene(QUENE* pQ, int *pVal) {
    if (empty_quene(pQ)) { // 如果队列空,则不能出队
        return 0;
    } else {
        *pVal = pQ->pBase[pQ->front]; // 获取队首元素
        pQ->front = (pQ->front + 1) % 6; // 更新队首索引
        return 1; // 出队成功
    }
}

// 图解:
// 出队操作:
// 如果队列为 [10, b, c, d, e, f]  (rear = 1, front = 0)
// 出队:
// 队列变为 [NULL, b, c, d, e, f]  (rear = 1, front = 1)
7. 遍历
// 遍历队列并输出所有元素
// 从队首到队尾输出元素

void traverse_quene(QUENE* pQ) {
    int i = pQ->front; // 从队首开始
    while (i != pQ->rear) { // 当队首索引不等于队尾索引时
        printf("%d ", pQ->pBase[i]); // 输出当前元素
        i = (i + 1) % 6; // 更新索引
    }
    printf("\n"); // 输出换行符
}

// 图解:
// 遍历队列:
// 如果队列为 [10, 20, 30, 40, 50, NULL]  (rear = 5, front = 0)
// 遍历输出:
// 10 20 30 40 50
8. 应用

队列的具体应用:

 所有和事件有关的操作都有队列的影子。(例如操作系统认为先进来的先处理)

// 队列的应用:
// 队列是先进先出的数据结构,适用于所有需要按顺序处理元素的场景
// 比如时间管理、任务调度等

// 图解:
// 先进先出 (FIFO):
// 入队: [A] -> [B] -> [C] -> [D]
// 出队: [B] -> [C] -> [D]  (A 已被移除)

 专题:递归

【这对你的编码能力是个质的飞跃,如果你想成为一个很厉害的

    程序员,数据结构是必须要掌握的,因为计算机专业的本科生也达不到这水

    平!计算机特别适合用递归的思想来解决问题,但是我们人类用递归的思想

    来考虑问题就会感到十分困扰,这也是很多学过递归的人一直都搞不明白的

    地方!那是不是递归可以随便写,当然不是,有些同学一用递归程序就死翘

    翘了。递归的思想是软件思想的基本思想之一,在树和图论上面,几乎全是

    用递归来实现的,最简单,像求阶乘这种没有明确执行次数的问题,都是用

    递归来解决】

定义:

 一个函数自己直接或间接调用自己(一个函数调用另外一个函数和他调用自己是一模一样的,都是那三步,只不过在人看来有点诡异。)

 递归满足的三个条件:

            1、递归必须得有一个明确的终止条件

            2、该函数处理的数据规模必须在递减

            3、这个转化必须是可解的。

循环和递归:

理论上循环能解决的,肯定可以转化为递归,但是这个过程是复杂的数学转化过程,递归能解决不一定能转化 为循环,我们初学者只要把经典的递归算法看懂就行,至于有没有能力运用看个人。    

递归:易于理解 ,速度慢,存储空间大

循环:不易于理解速度快 存储空间小            

举例:   

            1.求阶乘

            2.1+2+3+4+。。。+100的和

            3.汉诺塔

#include <stdio.h>

/*
汉诺塔问题的递归解法:
1. 如果只有一个盘子,直接将其从源柱子移动到目标柱子
2. 否则,将 n-1 个盘子从源柱子借助辅助柱子移动到目标柱子
3. 将第 n 个盘子从源柱子直接移动到目标柱子
4. 将 n-1 个盘子从辅助柱子借助源柱子移动到目标柱子
*/

void hannuota(int n, char A, char B, char C); // 函数声明

void main() {
    char ch1 = 'A'; // 源柱子
    char ch2 = 'B'; // 辅助柱子
    char ch3 = 'C'; // 目标柱子
    int n; // 盘子的数量

    printf("请输入要移动的盘子个数:");
    scanf_s("%d", &n); // 读取盘子数量
    hannuota(n, ch1, ch2, ch3); // 调用递归函数解决汉诺塔问题
    return;
}

void hannuota(int n, char A, char B, char C) {
    /*
    汉诺塔的递归解法:
    1. 递归基:如果只有一个盘子,直接移动
    2. 递归步骤:
       - 将 n-1 个盘子从柱子 A 移动到柱子 B,借助柱子 C
       - 将第 n 个盘子从柱子 A 移动到柱子 C
       - 将 n-1 个盘子从柱子 B 移动到柱子 C,借助柱子 A
    */

    if (1 == n) {
        printf("将编号为%d 的盘子直接从 %c 柱子移动到 %c 柱子\n", n, A, C);
    } else {
        hannuota(n - 1, A, C, B); // 先将 n-1 个盘子从 A 移动到 B
        printf("将编号为%d 的盘子直接从 %c 柱子移动到 %c 柱子\n", n, A, C); // 移动第 n 个盘子
        hannuota(n - 1, B, A, C); // 再将 n-1 个盘子从 B 移动到 C
    }
}

            【汉诺塔】这不是线性递归,这是非线性递归!

            n=1      1

            n=2      3

            n=3      7

            .........

            .........

            n=64     2的64次方减1【这是个天文数字,就算世界上最快的计算机

            也解决不了,汉诺塔的负责度是2的n次方减1】问题很复杂,但真正解决

            问题的编码只有三句。

            4.走迷宫(CS的实现)

            递归的运用:

                树和森林就是以递归的方式定义的

                树和图的很多算法都是以递归来实现的

                很多数学公式就是以递归的方式定义的

                   斐波拉契序列

                       1 2 3 5 8 13 21 34。。。                 

为何数据结构难学:因为计算机内存是线性一维的,而我们要处理的数据

都是比较复杂的,那么怎么把这么多复杂的数据保存在计算机中来保存本

身就是一个难题,而计算机在保存线性结构的时候比较好理解,尤其是数

组和链表只不过是连续和离散的问题,线性结构是我们学习的重点,因为

线性算法比较成熟,无论C++还是Java中都有相关的工具例如Arraylist.

Linkedlist,但是在Java中没有树和图,因为非线性结构太复杂了,他的

操作远远大于线性结构的操作。即使SUN公司也没造出来。                  

模块二:非线性结构

(现在人类还没有造出一个容器,能把树和图 都装进去的,因为他们确实是太复杂了)都要靠链表去实现)

   树

 树定义


1.    有且只有一个称为根的节点
2.    有若干个互不相交的子树,这些子树本身也是一棵树。
通俗的定义:
1.    树是由节点和边组成。
2.    每个节点只有一个父节点但可以有多个子节点
3.    但有一个节点例外,该节点没有父节点,此节点称为根节点。

专业术语             

  • 节点:树结构中的基本元素,每个节点包含数据和指向其他节点的链接(子节点)。

  • 根节点:树的顶端节点,没有父节点。每棵树都有一个唯一的根节点。

  • 父节点:直接连接到其他节点的节点,称为子节点的父节点。父节点可以有一个或多个子节点。

  • 子节点:直接由父节点连接的节点。子节点与父节点具有直接的父子关系。

  • 子孙节点:从某一节点出发,通过不断地向下跟随子节点,可以到达的所有节点。包括子节点、孙节点、曾孙节点等。

  • 堂兄弟节点:其父节点是另一个节点的子节点的节点。即堂兄弟节点的父节点与兄弟节点的父节点是兄弟关系。

  • 深度:从根节点到某个节点的路径长度(层数)。根节点的深度为0,子节点的深度比父节点多1。

  • 叶子节点:没有任何子节点的节点。叶子节点位于树的最底层。

  • 非终端节点:也称为非叶子节点,有至少一个子节点的节点。

  • :一个节点的子节点数量称为该节点的度。树的度是所有节点度数中的最大值。

分类

一般树

定义:任意一个节点的子节点的个数不受限制。

特点:树的结构比较灵活,节点的子节点数可以变化,因此它可以表示各种不同的层次关系。

二叉树(有序树)

定义:任意一个节点的子节点数量最多为两个,且子节点的位置不可更改(通常分为左子节点和右子节点)。

特点:二叉树是最常用的树结构之一,因其简单性和高效性被广泛使用。

分类

一般二叉树

定义:每个节点最多有两个子节点,且子节点的排列没有特定要求。

特点:这种树的形态没有特殊的限制,节点之间的关系比较灵活。

满二叉树

定义:在不增加树的层数的前提下,无法再多添加一个节点的二叉树。

特点:每个非叶子节点都有两个子节点,所有叶子节点都在同一层。

完全二叉树

定义:如果只是删除了满二叉树最底层最右边的连续若干个节点,这样形成的二叉树就是完全二叉树。

特点:除了最底层外,每层都被完全填满,最底层节点从左到右排列。

森林

定义:n个互不相交的树的集合。

特点:森林可以看作是一组独立的树的集合,每棵树之间没有连接关系。森林是将一棵树分割成多个树的一种形式,通常用于解决一些分离子问题的场景。

一般的二叉树要以数组的方式存储,要先转化成完全二叉树,因为如果你只存有效节点(无论先序,中序,后序),则无法知道这个树的组成方式是什么样子的。

树的存储(都是转化成二叉树来存储)

       

                      

      

代码

1. 结构体声明和函数声明
#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>

// 二叉树节点的结构体声明
typedef struct BTNode {
    int data; // 节点的数据
    struct BTNode* pLchild; // 指向左子节点的指针
    struct BTNode* pRchild; // 指向右子节点的指针
} BTNODE, *PBTNODE; // BTNODE 是结构体类型的别名,PBTNODE 是指向 BTNODE 的指针类型

// 函数声明
void InTraverseBTree(PBTNODE pT); // 中序遍历
void PreTraverseBTree(PBTNODE pT); // 前序遍历
void PostTraverseBTree(PBTNODE pT); // 后序遍历
PBTNODE CreatBtree(void); // 创建二叉树

2. 主函数 

void main() {
    PBTNODE pT = CreatBtree(); // 创建二叉树

    printf("前序遍历:\n");
    PreTraverseBTree(pT); // 进行前序遍历
    
    printf("中序遍历:\n");
    InTraverseBTree(pT); // 进行中序遍历
    
    printf("后序遍历:\n");
    PostTraverseBTree(pT); // 进行后序遍历
}

3. 创建二叉树

// 创建一个简单的二叉树并返回根节点
PBTNODE CreatBtree(void) {
    // 分配内存并初始化节点
    PBTNODE pA = (PBTNODE)malloc(sizeof(BTNODE));
    PBTNODE pB = (PBTNODE)malloc(sizeof(BTNODE));
    PBTNODE pC = (PBTNODE)malloc(sizeof(BTNODE));
    PBTNODE pD = (PBTNODE)malloc(sizeof(BTNODE));
    PBTNODE pE = (PBTNODE)malloc(sizeof(BTNODE));
    
    // 初始化节点数据
    pA->data = 'A'; // 根节点
    pB->data = 'B'; // 左子节点
    pC->data = 'C'; // 右子节点
    pD->data = 'D'; // C 的左子节点
    pE->data = 'E'; // D 的左子节点
    
    // 设置节点的左右子节点
    pA->pLchild = pB; pA->pRchild = pC; // A 的左子节点是 B,右子节点是 C
    pB->pLchild = pB->pRchild = NULL; // B 没有子节点
    pC->pLchild = pD; pC->pRchild = NULL; // C 的左子节点是 D,没有右子节点
    pD->pLchild = pE; pD->pRchild = NULL; // D 的左子节点是 E,没有右子节点
    pE->pLchild = pE->pRchild = NULL; // E 没有子节点

    // 返回根节点
    return pA;
}
       A
      / \
     B   C
        / 
       D
      /
     E

4. 前序遍历

// 前序遍历:根 -> 左 -> 右
void PreTraverseBTree(PBTNODE pT) {
    if (pT != NULL) {
        printf("%c\n", pT->data); // 访问根节点
        
        // 递归遍历左子树
        if (pT->pLchild != NULL) {
            PreTraverseBTree(pT->pLchild);
        }
        
        // 递归遍历右子树
        if (pT->pRchild != NULL) {
            PreTraverseBTree(pT->pRchild);
        }
    }
}
A -> B -> C -> D -> E

 5. 中序遍历 

// 中序遍历:左 -> 根 -> 右
void InTraverseBTree(PBTNODE pT) {
    if (pT != NULL) {
        // 递归遍历左子树
        if (pT->pLchild != NULL) {
            InTraverseBTree(pT->pLchild);
        }
        
        printf("%c\n", pT->data); // 访问根节点
        
        // 递归遍历右子树
        if (pT->pRchild != NULL) {
            InTraverseBTree(pT->pRchild);
        }
    }
}
B -> A -> E -> D -> C

6. 后序遍历

// 后序遍历:左 -> 右 -> 根
void PostTraverseBTree(PBTNODE pT) {
    if (pT != NULL) {
        // 递归遍历左子树
        if (pT->pLchild != NULL) {
            PostTraverseBTree(pT->pLchild);
        }
        
        // 递归遍历右子树
        if (pT->pRchild != NULL) {
            PostTraverseBTree(pT->pRchild);
        }
        
        printf("%c\n", pT->data); // 访问根节点
    }
}
B -> E -> D -> C -> A

  • 16
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值