目录
增加/插入:在数组的某个位置插入新元素,需要将插入位置之后的元素向右移动。
数据结构的定义
我们如何把现实中大量而复杂的问题以特定的数据类型(单 个数据怎样存储?)和特定的存储结构(个体的关系) 保存到主存储器(内存)中,以及在此基础上为实现某个功能(比如查找某个元素,删除某个元素,对所有元素进行排序)而执行的相应操作,这个相应的操作也叫算法。(比如班里有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