本篇内容是根据郝斌老师数据结构的学习笔记
如有侵权或总结中有任何错误愿意通过邮箱,QQ联系我的话,本人将万分感谢。
邮箱:[email protected]
QQ:227222517
我们如何把现实中大量而复杂的问题以特定的数据类型(个体如何来存储)和特定存储结构(个体和个体的的关系)保存到主存储器(内存)中,以及在此基础上为实现某功能(eg:比如查找某个元素,删除某个元素,对所有元素进行排序而执行的相应操作,这个相应的操作也叫算法。
数据结构=个体的存储+个体的关系存储
算法=对存储数据的操作
数据结构狭义上的理解:
数据结构是专门研究数据存储的问题
数据结构的存储包含两个方面:个体的存储 + 个体关系的存储
数据结构广义上的理解:
数据结构即包含数据结构的存储也包含数据结构的操作
对存储数据的操作就是算法
算法->狭义
算法和数据的存储方式密切相关
算法->广义
算法和数据结构的存储方式无关
这就是泛型的思
1.线性
优点:
存储速度很快
存储效率高
缺点:
插入,删除元素很慢
需要大块连续的内存块
事先必须知道数组的长度
空间通常是有限制的
优点:
空间没有限制(存储容量没有限)
插入,删除元素很快
缺点 :
存储元素速度慢
1.时间复杂度:大概程序要执行的次数,而非执行的时间
2.空间复杂度:算法执行过程中大概所占用的最大内存
3.难易程
4.健壮性
数据结构是软件中最核心的课程
程序=数据的存储+数据的操作+可以被计算机执行的语言
# include
int main(void)
{
int * p; //p是一个变量名字,int * 表示该p变量只能存储int类型变量的地址
int i = 10;
int j;
p = &i;
j = *p;//等价于 j = i;
printf("i = %d, j = %d, *p = %d\n", i, j, *p);
//p = 10; //error
return 0;
}
//运行结果i = 10,j = 10,*p = 10
p 保存i的地址,那么 p 就指向 i ;
修改 p 的值不影响 i 的值,修改 i 的值不影响p 的值;
*p就代表了 i ,因为 p 指向i ;
如果 p 指向 j ,*p 就代表 j ;
*p 等价于 p 指向的变量;
# include
int main(void)
{
int * p; //p是一个变量名字,int * 表示该p变量只能存储int类型变量的地址
int i = 10;
int j;
p = &i;
*p = i;
//j = *p; //等价于 j = i;
printf("i = %d, j = %d, *p = %d\n", i, j, *p);
return 0;
}
//运行结果:i = 10, j = 327649(垃圾数字), *p = 10
1.内存是用来存储数据的设备。它的速度介于寄存器和硬盘之间。
2.内存是CPU唯一可以访问的大容量的存储设备!所有硬盘中的程序和数据必须调入内存之后方可被CPU执行!
切记:CPU不能直接处理硬盘中的数据!
3.内存的问题是软件开发中最核心的问题之一!如:内存的分配,内存的释放,内存在什么时候分配,内存什么时候释放,由谁来分配,由谁来释放,分配在什么地方,访问权限如何!
4.内存是多字节组成的线性一维存储空间
5.内存的基本划分单位是字节
6.每个字节含有8位。(一个直接占8位)每一位存放1个0或1个1
7.字节和编号是一 一对应的。每个字节都有一个唯一确定的编号,一个编号对应一个字节!这个编号也叫地址
8.一个系统所能管理的内存空间的大小取决于参与编号的二进制位数!
如:
DOS 系统20位寻址方式,可控2020即1MB
386/486系统采用三字节编制,可寻址2024即16MB的内存
奔腾寻址空间32位,其有效寻址空间位2032即4GB
内存是操作系统的统一管理下使用的!
1.软件在运行前需要向操作系统申请存储空间,在内存空闲足够时,操作系统将分配一段内存空间并将外村中软件拷贝一份存入内存空间中,并启动该软件的运行!
2.在软件运行期间,该软件所占内存空间不在分配给其他软件
3.当软件运行完毕后,操作系统将回收该内存空间(注意:操作系统并不清空该存储空间中遗留下来的数据),已便于再次分配给其他软件使用
综上所述,一个软件所分配的空间中极可能存在着以前其他软件使用过后的残留数据,这些数据被之为垃圾数据。所以通常情况下我们为一个变量,为一个数组,分配好存储空间之后都要丢该内存空间初始化!
# include
void f(int * p) //不是定义了一个名字叫*p的形参,而是定义了一个形参,该形参名字叫做p,它的类型是int *
{
*p = 100; //
}
int main(void)
{
int i = 9;
f(&i);
printf("i = %d\n", i);
return 0;
}
//i = 100
数组名
一维数组名是个指针常量,他存放的是一维数组第一个元素的地址,他的值不能被改
一维数组名指向的是数组的第一个元素
下标和指针的关系
a[i]<<==>> *(a+i)
假设指针变量的名字为p
则p+i的值是p+i*(p所指向的变量所占的字节数)
指针变量不能相加,不能相乘,不能相除
如果两指针变量属于同一数组,则可以相减
指针变量可以加减一整数,前提是最终结果不能超过指针长度
p+i的值是p+i*(p所指向的变量所占的字节数)
p-i的值是p-i*(p所指向的变量所占的字节数)
p++ <==> p+1
p-- <==> p-1
如何通过被调函数修改主调函数中一维数组的内容
存放数组首元素的指针变量
存放数组元素长度的整型变量
# include
void Show_Array(int * p, int len)
{
int i = 0;
for (i = 0; i < len; ++i)
printf("%d\n", p[i]);
//p[2] = -1; p[0] == *p p[2] == *(p + 2) == *(a + 2) == a[2];
//p[i]就是主函数的a[i]
}
int main(void)
{
int a[5] = { 1,2,3,4,5};
Show_Array(a, 5);//等价于&a[0],&a[0]本身就是int *类型
//printf("%d\n", a[2]);
return 0;
}
//运行结果:1 2 3 4 5
为了表示一些复杂的数据,而普通的基本类型变量无法满足要求
结构体是用户根据实际需要自己定义的复合数据类型
# include
#include
struct Student
{
int sid;
char name[200];
int age;
};//分号不能省略
int main(void)
{
struct Student st = { 1000,"zhangsan",20 };
printf("%d %s %d\n", st.sid, st.name, st.age);
st.sid = 99;
//st. name =" lisi" ¡ / /error
strcpy(st.name, "lisi");
st.age = 22;
printf(" %d %s %d\n", st.sid, st.name, st.age);
//printf(" %d %s %d\n", st);//error
return 0;
}
/*运行结果:1000 zhangsan 20
99 lisi 22*/
struct Student st = { 1000,"zhangsan", 20};
struct Student* pst = &st;
1.通过结构体变量名来实现
st.sid
2.通过指向结构体变量的指针来实现【重点】
pst->sid
pst所指向的结构体变量中的sid这个成员
# include<stdio.h>
struct Student
{
int sid;
char name[200];
int age;
};//分号不能省略
int main(void)
{
struct Student st = { 1000,"zhangsan",20 };
//st.sid = 99;//第一种方式
struct Student* pst;
pst = &st;
pst->sid = 99; //第二种方式pst->sid等价于(*pst).sid 而(*pst).sid等价于st.sid,所以pst->sid等价于st.sid
return 0;
}
代码实现:
# include
#include
struct Student
{
int sid;
char name[200];
int age;
};//分号不能省略
void f(struct Student* pst);
void g(struct Student st);
void g2(struct Student* pst);
int main(void)
{
struct Student st;//已经为st分配好了内存
f(&st);
g2(&st);
//printf("%d %s %d\n", st.sid, st.name, st.age);
return 0;
}
//这种方式耗费内存,耗费时间,不推荐
void g(struct Student st)
{
printf("%d %s %d\n", st.sid, st.name, st.age);
}
void g2(struct Student* pst)
{
printf("%d %s %d\n", pst->sid, pst->name, pst->age);
}
void f(struct Student* pst)//把st的地位发送给了pst;
{
(*pst).sid = 99;
strcpy(pst->name, "zhangsan");
pst->age = 22;
}
//运行结果:99 zhangsan 22
结构体变量不能加减乘除,但可以相互赋值
普通结构体变量和结构体指针变量作为函数传参的问题
动态构造一维数组
假设动态构造一个int型数组
int* p = (int *)malloc(int len)
int* p = (int*)malloc(50);
p就指向了第1个的四个字节,p+1就指向了第2个的四个字节,p+i就指向了第i+1个的4个字节。p[0]就是第1个元素,p[1]就是第2个元素,p[i]就是i+1个元素。
#include
#include
int main(void)
{
int a[5] = { 4,10,2,8,6 };
int len;
printf("请输入你需要分配的数组的长度:len= ");
scanf("%d", &len);
int* pArr = (int*)malloc(sizeof(int) * len);
//*pArr = 4;//类似于a[0] = 4;
//pArr[1] = 10;//类似于a[1] = 10;
//printf("%d %d\n", *pArr, pArr[1]);
//我们可以把pArr党做一个普通数组来使用
for (int i = 0; i < len; ++i)
scanf("%d", &pArr[i]);
for (i = 0; i < len; ++i)
printf("%d\n", *(pArr + i));
free(pArr);//把pArr所代表的动态分配的20个字节的内存释放
return 0;
}
/*运行结果:请输入你需要分配的数组的长度:len=5
1 2 3 4 5
1
2
3
4
5*/
例题:(选择题)下程序中,能够通过调用函数fun,使用main函数中的指针变量p指向一个合法的==整型单元(变量)==的是?
A)error
main()
{
int* p;//定义了一个p,p里面是垃圾数字
fun(p);//直接写p没有办法修改p
...
}
int fun(int* p)
{
int s;
p = &s;
}
B)error
main()
{
int* p;//p是int*类型
fun(&p); //取地址p是int**类型
...
}
int funn(int** q) //把取地址p发送给q
{
int s;
*q = &s;//把s的地址发送给*p,此时p的指向合法
}//当函数执行完毕p的指向不合法
C)exactness
main()
{
int* p;//p是一个指针变量
fun(&p);//p变量的地址发送给q,所以*q就是p
...
}
int fun(int** q)
{
*q = (int*)malloc(4);
}//c语言中通过malloc()动态分配的内存必须手动通过free()释放
D)error
main()
{
int* p;
fun(p);//error
...
}
int fun(int* p)
{
p = (int*)malloc(sizeof(int));
}
A aa = new A();
A *pa = (A *)malloc(sizeof(A));//是一样的
#include
#include
struct Student
{
int sid;
int age;
};
struct Student* CreateStudent(void);
void ShowStudent(struct Student *);
int main(void)
{
struct Student* ps;
ps = CreateStudent();
ShowStudent(ps);
return 0;
}
void ShowStudent(struct Student* pst)
{
printf("%d %d\n", pst->sid, pst->age);
}
struct Student* CreateStudent(void)
{
struct Student* p = (struct Student *)malloc(sizeof(struct Student));
p->sid = 99;
p->age = 88;
return p;
}
//运行结果:99 88
证明了跨函数使用内存
int a[10];
int* pArr = (int*)malloc(sizeof(int) * len);//数组
元素类型相同,大小相等
优点:存取速度很快
缺点:插入删除元素很慢
# include
# include
# include
//定义了一个数据类型,该数据类型的名字叫struct Arr,该数据类型分别有三个成员pBase,len,cnt
struct Arr
{
int* pBase;//存储的是数组第一个元素的地址
int len; //表示数组所能容纳的最大元素的个数
int cnt; //当前数组有效元素的个数
//int increment; //自动增长因子
};
void init_arr(struct Arr* pArr, int length); //初始化,分号不能省略
bool append_arr(struct Arr* pArr,int val); //追加
bool insert_arr(struct Arr* pArr, int pos,int val); //插入 //pos的值从1开始
bool delete_arr(struct Arr* pArr, int pos, int* pVal); //删除
int get(); //获取下标为...的某一个值
bool is_empty(struct Arr* pArr);//判断是否为空
bool is_full(struct Arr* pArr); //判断是否满
void sort_arr(struct Arr* pArr); //排序
void show_arr(struct Arr* pArr); //显示,输出
void inversion_arr(struct Arr* pArr); //倒置
int main(void)
{
struct Arr arr;
int val;
init_arr(&arr, 6);
show_arr(&arr);
append_arr(&arr, 1);
append_arr(&arr, 10);
append_arr(&arr, -3);
append_arr(&arr, 6);
append_arr(&arr, 88);
append_arr(&arr, 11);
if (delete_arr(&arr, 1, &val))
{
printf("删除成功!\n");
printf("您删除的元素是:%d\n", val);
}
else
{
printf("删除失败!\n");
}
/* append_arr(&arr, 2);
append_arr(&arr, 3);
append_arr(&arr, 4);
append_arr(&arr, 5);
insert_arr(&arr, 1, 99);
append_arr(&arr, 6);
append_arr(&arr, 7);
if (append_arr(&arr, 8))
{
printf("追加成功!\n");
}
else
{
printf("追加失败!\n");
}*/
show_arr(&arr);
inversion_arr(&arr);
printf("倒置之后的数组内容是:\n");
show_arr(&arr);
sort_arr(&arr);
show_arr(&arr);
//printf("%d\n", arr.len);
return 0;
}
void init_arr(struct Arr* pArr,int length)
{
pArr->pBase = (int*)malloc(sizeof(int) * length);
if (NULL == pArr->pBase)
{
printf("动态内存分配失败!\n");
exit(-1); //终止整个程序
}
else
{
pArr->len = length;
pArr->cnt = 0;
}
return;
}
bool is_empty(struct Arr* pArr)
{
if (0 == pArr->cnt)
return true;
else
return false;
}
bool is_full(struct Arr* pArr)
{
if (pArr->cnt == pArr->len)
return true;
else
return false;
}
void show_arr(struct Arr* pArr)
{
if (is_empty(pArr))
{
printf("数组为空!\n");
}
else
{
for (int i = 0; i < pArr->cnt; ++i)
printf("%d ", pArr->pBase[i]); //int *
printf("\n");
}
/*if(数组为空)
提示用户数组为空
else
输出数组有效内容*/
}
bool append_arr(struct Arr* pArr, int val)
{
//满时返回false
if (is_full(pArr))
return false;
//不满时追加
pArr->pBase[pArr->cnt] = val;
(pArr->cnt)++;
return true;
}
bool insert_arr(struct Arr* pArr, int pos, int val)
{
int i;
if (is_full (pArr))
return false;
if (pos<1 || pos>pArr->cnt + 1)
return false;
for (i = pArr->cnt - 1; i >= pos - 1; --i)
{
pArr->pBase[i + 1] = pArr->pBase[i];
}
pArr->pBase[pos - 1] = val;
(pArr->cnt)++;
return true;
}
bool delete_arr(struct Arr* pArr, int pos, int* pVal)
{
int i;
if (is_empty(pArr))
return false;
if (pos<1 || pos>pArr->cnt)
return false;
*pVal = pArr->pBase[pos - 1];
for (i = pos; i < pArr->cnt; ++i)
{
pArr->pBase[i - 1] = pArr->pBase[i];
}
pArr->cnt--;
return true;
}
void inversion_arr(struct Arr* pArr)
{
int i = 0;
int j = pArr->cnt - 1;
int t;
while (i < j)
{
t = pArr->pBase[i];
pArr->pBase[i] = pArr->pBase[j];
pArr->pBase[j] = t;
++i;
--j;
}
return;
}
void sort_arr(struct Arr* pArr)
{
int i, j, t;
for (i = 0; i < pArr->cnt; ++i)
{
for (j = i + 1; j < pArr->cnt; ++j)
{
if (pArr->pBase[i] > pArr->pBase[j])
{
t = pArr->pBase[i];
pArr->pBase[i] = pArr->pBase[j];
pArr->pBase[j] = t;
}
}
}
}
/*运行结果:
数组为空!
删除成功!
您删除的元素是:1
1 0 -3 6 88 11
倒置之后的数组内容是:
11 88 6 -3 10
-3 6 10 11 88
n个节点离散分配
彼此通过指针相连
每个节点只有一个前驱节点,每个节点只有一个后续节点
首节点没有前驱节点 尾节点没有后续节点
首节点: 第一个有效节点
尾节点: 最后一个有效节点
头结点:
头节点的数据类型和首节点类型一样
第一个有效节点之前的那个节点
头结点并不存放有效数据
加头结点的目的主要是为了方便对链表的操作
头指针: 指向头结点的指针变量
尾指针: 指向尾节点的指针变量
如果希望通过一个函数来对链表进行处理,我们至少需要接受链表的哪些参数:
答:只需要一个参数,头指针。因为我们通过头指针可以推算出链表的其他所有参数。
#include
typedef struct Node
{
int data; //数据域,储存数据本身
struct Node* pNext; //指针域,指针域pNext指向的是跟他本身数据类型一样的另外一个节点,pNext指向一个和它本身储存指向下一个节点的指针
}NODE, * PNODE; //NODE等价于struct Node, * PNODE等价于struct Node*
int main(void)
{
PNODE p = (PNODE)malloc(sizeof(NODE)); //将动态分配的新节点地址赋给p
free p; //删除p指向节点所占的内存,不是删除p本身所占内存
p->pNext; //p所指向结构体变量中的pNext成员本身
return 0;
}//模拟节点,节点存放有效数据和指针
r = p->pNext; p->pNext = q; q->pNext = r;
类比于把A的值于B的值互换
q->pNext = p->pNext; p->pNext = q;
先临时定义一个指向p后面节点的指针r
r = p->pNext; //r指向p后面的那个节点
p->pNext = r->pNext;
free (r);
(让第一个节点指向第三个节点的pNext,但是第二个节点会丢失,无法释放该节点)
分类
算法
#include
#include
#include
typedef struct Node
{
int data; //数据域
struct Node* pNext; //指针域
}NODE, * PNODE; //NODE等价于struct Node,PNODE等价于struct Node*
//函数声明
PNODE create_list(void);
void traverse_list(PNODE pHead);
bool is_empty(PNODE pHead);
int length_list(PNODE);
bool insert_list(PNODE, int, int); //在pHead所指向链表的第pos个节点的前面插入一个新的节点,该节点的值是val,并且pos的值是从1开始
bool delete_list(PNODE, int, int*);
//delete_list(pHead, 3, &val); //指把pHead指向链表的第三个删除,删除后放到&val,把&val[val地址]发送给第三个参数
void sort_list(PNODE);
int main(void)
{
PNODE pHead = NULL; //等价于struct Node * pHead = NULL;
int val;
pHead = create_list(); //ereate_ list()功能:创建一个非循环单链表,并将该链表的
traverse_list(pHead);
// insert_list(pHead, 4, 33);
if (delete_list(pHead, 4, &val))
{
printf("删除成功,您删除的元素是: %d\n", val);
}
else
{
printf("删除失败!您删除的元素不存在!\n");
}
traverse_list(pHead);
// int len = length_list(pHead);
// printf("链表的长度是%d\n", len);
// sort_list(pHead);
// traverse_list(pHead);
/* if(is_empty(pHead))
printf("链表为空!\n");
else
printf("链表不空!\n");
*/
return 0;
}
PNODE create_list(void)
{
int len; //用来存放有效节点的个数
int i;
int val; //用来临时存放用户输入的结点的值
//分配了一个不存放有效数据的头结点
PNODE pHead = (PNODE)malloc(sizeof(NODE));
if (NULL == pHead)
{
printf("分配失败,程序终止!\n");
exit(-1);
}
PNODE pTail = pHead;
pTail->pNext = NULL;
printf("请输入您需要生成的链表节点的个数:len =");
scanf("%d", &len);
for (i = 0; i < len; ++i)
{
printf("请输入第%d个节点的值:", i + 1);
scanf("%d", &val);
PNODE pNew = (PNODE)malloc(sizeof(NODE));
if (NULL == pNew)
{
printf("分配失败,程序终止!\n");
exit(-1);
}
pNew->data = val;
pTail->pNext = pNew;
pNew->pNext = NULL;
pTail = pNew;
}
return pHead;
}
void traverse_list(PNODE pHead)
{
PNODE p = pHead->pNext;
while (NULL != p)
{
printf("%d ", p->data);
p = p->pNext;
}
printf("\n");
return;
}
bool is_empty(PNODE pHead)
{
if (NULL == pHead->pNext)
return true;
else
return false;
}
int length_list(PNODE pHead)
{
PNODE p = pHead->pNext;
int len = 0;
while (NULL != p)
{
++len;
p = p->pNext;
}
return len;
}
void sort_list(PNODE pHead)
{
int i, j, t;
int len = length_list(pHead);
PNODE p, q;
for (i = 0, p = pHead->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) //类似于数组中的:a[i] > a[j]
{
t = p->data; //类似于数组中的: t=a[i];
p->data = q->data; //类似于数组中的a[i] = a[j];
q->data = t; //类似于数组中的: a[j] = t;
}
}
}
return;
}
//在pHead所指向链表的第pos个节点的前面插入一个新的节点,该节点的值是val,并且pos的值是从1开始
bool insert_list(PNODE pHead, int pos, int val)
{
int i = 0;
PNODE p = pHead;
while (NULL!=p && i < pos - 1)
{
p = p->pNext;
++i;
}
if (i > pos - 1 || NULL == p)
return false;
PNODE pNew = (PNODE)malloc(sizeof(NODE));
if (NULL == pNew)
{
printf("动态分配内存失败!\n");
exit(-1);
}
pNew->data = val;
PNODE q = p->pNext;
p->pNext = pNew;
pNew->pNext = q;
return true;
}
bool delete_list(PNODE pHead, int pos, int* pVal)
{
int i = 0;
PNODE p = pHead;
while (NULL != p->pNext && i < pos - 1)
{
p = p->pNext;
++i;
}
if (i > pos - 1 || NULL == p->pNext)
return false;
PNODE q = p->pNext;
*pVal = q->data;
//删除p节点后面的节点
p->pNext = p->pNext->pNext;
free(q);
q = NULL;
return true;
}
/*本代码是为实验链表的插入删除
输入链表节点个数,以及各个链表节点值,可以删除,或者查出任意节点
*/
算法:
狭义的算法是与数据的存数方式密切相关
广义的算法是与数据的存储方式无关
泛型:
利用某种技术达到的效果就是:不同的存数方式,执行的操作是一样的
#include
typedef int ZHANGSAN; //为int在重新多取一个名字,ZHANGSAN等价于int
typedef struct Student
{
int sid;
char name[100];
char sex;
}ST;
int main(void)
{
//int i = 10; //等价于ZHANGSAN i = 10;
//ZHANGSAN j = 20;
//printf("%d\n", j);
struct Student st; //等价于ST st;
struct Student* ps = &st; //等价于ST* ps;
ST st2;
st2.sid = 200;
printf("%d\n", st2.sid);
return 0;
}
//运行结果:200
#include
typedef struct Student
{
int sid;
char name[100];
char sex;
}*PST, STU; //等价于STU代表了struct Student, PST代表了struct Student*
int main(void)
{
STU st; //等价于struct Student st;
PST ps = &st; //等价于struct Student* ps = &st;
ps->sid = 99;
printf("%d\n", ps->sid);
return 0;
}
//运行结果:99
栈和堆,分配内存的方式不一样
动态分配由堆分配,静态分配由栈分配。
一种可以实现“先进后出”的存储结构
栈类似于箱子
静态栈
动态栈
出栈
压栈
#include
#include
#include
typedef struct Node
{
int data; //数据域
struct Node* pNext; //指针域
}NODE,*PNODE;
typedef struct Stack
{
PNODE pTop; //指向栈顶元素
PNODE pBottom; //指向栈底元素下一个没有实际含义的元素,头节点
}STACK,*PSTACK; //PSTACK等价于struct STACK*
void init(PSTACK); //完成一个初始化
void push(PSTACK, int);
void traverse(PSTACK);
bool pop(PSTACK, int*);
void clear(PSTACK);
int main(void)
{
STACK S; //STACK等价于struct Stack
int val;
init(&S); //目的是造出一个空栈
push(&S, 1); //压栈
push(&S, 2);
push(&S, 3);
push(&S, 4);
push(&S, 5);
push(&S, 6);
traverse(&S); //遍历输出
clear(&S);
// traverse(&S); //遍历输出
if (pop(&S, &val))
{
printf("出栈成功,出栈的元素是%d\n", val);
}
else
{
printf("出栈失败!\n");
}
traverse(&S); //遍历输出
return 0;
}
void init(PSTACK pS) //目的是让pTop,pBottom指向头节点,生产一个空栈
{
pS->pTop = (PNODE)malloc(sizeof(NODE));
if (NULL == pS->pTop)
{
printf("分配内存失败!\n");
exit(-1);
}
else
{
pS->pBottom = pS->pTop;
pS->pTop->pNext = NULL; //== pS->pBottom->pNext = NULL;
}
return;
}
void push(PSTACK pS,int val)
{
PNODE pNew = (PNODE)malloc(sizeof(NODE)); //新节点
pNew->data = val;
pNew->pNext = pS->pTop; //pS->pTop不能改成pS->Bottom
pS->pTop = pNew;
return;
}
void traverse(PSTACK pS)
{
PNODE p = pS->pTop;
while (p != pS->pBottom)
{
printf("%d ", p->data);
p = p->pNext;
}
printf("\n");
return;
}
bool empty(PSTACK pS) //判断是否为空
{
if (pS->pTop == pS->pBottom)
return true;
else
return false;
}
//把pS所指向的栈出栈一次,并把出栈的元素存入pVal形参所指向的变量中,如果出栈失败返回false,否则返回true
bool pop(PSTACK pS, int* pVal)
{
if (empty(pS)) //pS本身存放的就是S的地址
{
return false;
}
else
{
PNODE r = pS->pTop;
*pVal = r->data;
pS->pTop = r->pNext;
free(r);
r = NULL;
return true;
}
}
//clear清空
void clear(PSTACK pS)
{
if (empty(pS))
return;
else
{
PNODE p = pS->pTop;
PNODE q = NULL;
while (p != pS->pBottom)
{
q = p->pNext;
free(p);
p = q;
}
pS->pTop = pS->pBottom;
}
return;
}
/*
运行结果:6 5 4 3 2 1
出栈失败
所有修改均在int main(void)
*/
函数调用
中断
表达式求值
内存分配
缓冲处理
迷宫
一种可以实现“先进先出”的存储结构
链式队列 -- 用链表实现
静态队列 -- 用数组实现
静态队列通常都必须是循环队列
1.静态队列为什么必须是循环队列
2.循环队列需要几个参数来确定
答:需要2个参数来确定
front
rear
3.循环队列各个参数的含义
2个参数不同场合有不同含义
建议初学者先记住,然后慢慢体会
1).队列初始化
front和rear的值都是零
2).队列非空
front代表的是队列的第一个元素
rear代表的是队列的最后一个有效元素的下一个元素
3).队列空
front和rear的值相等,但是不一定为零
4.循环队列入队伪算法讲解
两部完成:
1.将值存入r所代表的位置
2.错误的写法是:rear = rear + 1;
正确的写法是:rear = (rear+1) % 数组的长度
5.循环队列出队伪算法讲解
front = (front+1) % 数组的长度
6.如何判断循环队列是否为空
如果 front 与 rear 的值相等
则该队列一定为空
7.如何判断循环队列是否已满
front的值可能比rear大
也完全有可能比rear小
当然也可能相等
是没有规律的
1.多增加一个表示标识的参数
2.少用一个元素【通常使用第二种方式】
如果r和f的值紧挨着,则队列已满
用C语言伪算法表示:
if ((r+1)%数组长度 == f)
已满
else
不满
入队
出队
所有和时间有关的操作都有队列的影子
循环队列代码实现:
#include
#include
typedef struct Queue //定义了一个叫Queue结构体
{
int* pBase; //循环数组的首地址
int front; //充当下标
int rear; //充当下标
}QUEUE;
void init(QUEUE*); //初始化队列
bool en_queue(QUEUE*, int val); //入队
void traverse_queue(QUEUE*); //遍历队列,输出
bool full_queue(QUEUE*); //队列是否已满
bool out_queue(QUEUE*, int*); //出队
bool empty_queue(QUEUE*);
int main(void)
{
QUEUE Q;
int val;
init(&Q);
en_queue(&Q, 1);
en_queue(&Q, 2);
en_queue(&Q, 3);
en_queue(&Q, 4);
en_queue(&Q, 5);
en_queue(&Q, 6);
en_queue(&Q, 7);
en_queue(&Q, 8);
traverse_queue(&Q);
if (out_queue(&Q, &val))
{
printf("出队成功,队列出队的元素是: %d\n", val);
}
else
{
printf("出队失败!\n");
}
traverse_queue(&Q);
return 0;
}
void init(QUEUE* pQ)
{
pQ->pBase = (int*)malloc(sizeof(int) * 6); //假定长度为6
pQ->front = 0; //初始化
pQ->rear = 0; //初始化
}
bool full_queue(QUEUE* pQ)
{
if ((pQ->rear + 1) % 6 == pQ->front)
return true;
else
return false;
}
bool en_queue(QUEUE* pQ, int val)
{
if (full_queue(pQ))
{
return false;
}
else
{
pQ->pBase[pQ->rear] = val;
pQ->rear = (pQ->rear + 1) % 6;
return true;
}
}
void traverse_queue(QUEUE* pQ) //从头部遍历
{
int i = pQ->front;
while (i != pQ->rear)
{
printf("%d ", pQ->pBase[i]);
i = (i + 1) % 6;
}
printf("\n");
return;
}
bool empty_queue(QUEUE* pQ)
{
if (pQ->front == pQ->rear)
return true;
else
return false;
}
bool out_queue(QUEUE* pQ, int* pVal)
{
if (empty_queue(pQ))
{
return false;
}
else
{
*pVal = pQ->pBase[pQ->front];
pQ->front = (pQ->front + 1) % 6;
return true;
}
}
/*
运行结果:1 2 3 4 5
出队成功,队列出队的元素是:1
2 3 4 5
*/
一个函数自己直接或间接调用自己
1.递归必须得有一个明确的中止条件
2.该函数所处理的数据规模必须在递减
3.这个转化必须是可解的(个人理解)
递归:易于理解、速度慢、存储空间大
循环:不易理解、速度快、存储空间小
//不同函数之间的相互调用
#include
void f();
void g();
void k();
void f()
{
printf("FFFF\n");
g();
printf("1111\n");
}
void g()
{
printf("GGGG\n");
k();
printf("2222\n");
}
void k()
{
printf("KKKK\n");
}
int main(void)
{
f();
return 0;
}
/*
运行结果:FFFF GGGG KKKK 2222 1111
*/
//自己调用自己,必须声明什么时候不调用自己,否则会溢栈
#include
void f(int n)
{
if (n == 1)
printf("本函数没有实际意义,仅用于证明函数自己调用自己!\n");
else
f(n - 1);
}
int main(void)
{
f(3);
return 0;
}
//间接调用自己举例
#include
void f(int n)
{
g(n);
}
void g(int m)
{
f(m);
}
int main(void)
{
return 0;
}
//A函数调用B函数举例_1
#include
int f(int n)
{
n += 2; // n = n + 2;
return n;
}
int main(void)
{
int val;
val = f(5);
printf("val=%d\n", val);
return 0;
}
//运行结果:val-7
//A函数调用B函数举例_2
#include
int g(int);
int f(int n)
{
n = g(n);
return n;
}
int g(int m)
{
m = m * 2;
return m;
}
int main(void)
{
int val;
val = f(5);
printf("val=%d\n", val);
return 0;
}
//运行结果:val-10
//A函数调用B函数举例_3
#include
int g(int);
int f(int n)
{
if (n < 3)
printf("哈哈\n");
else
n = f(n - 1);
return n;
}
int g(int m)
{
m = m * 2;
return m;
}
int main(void)
{
int val;
val = f(5);
return 0;
}
//运行结果:哈哈
当在一个函数运行期间,调用另一个函数时,在运行被调用函数之前,系统需要先完成三件事.
第一:将所有的实际参数(实参),返回地址等信息传递给被调用函数保存。
第二:为被调函数的局部变量(也包括行参)分配存储空间。
第三:将控制转移到被调函数的入口。
第一:保存被调函数的返回结果。
第二:释放被调函数所占的存储空间。
第三:依照被调函数保存的返回地址将控制转移到调用函数。
当有多个函数相互调用时,按照”后调用先返回“的原则,上述函数之间信息传递和控制转移必须借助”栈“来实现。
即系统将整个程序运行时所需的数据空间安排在一个栈中,每当调用一个函数时,就在栈顶分配一个存储区,进行压栈操作,
每当一个函数退出时,就释放它的存储区,就做出栈操作,当前运行的函数永远都在栈顶位置。
A函数调用A函数和A函数调用B函数在计算机看来是没有任何区别的,只不过用我们日常的思维方式理解比较怪异而已!
//阶乘的循环使用,不可以写太大,否则会溢出
#include
int main(void)
{
int val;
int i, mult = 1;
printf("请输入您需要求阶乘的数字: ");
printf("val = ");
scanf("%d", &val);
for (i = 1; i <= val; ++i)
mult = mult * i;
printf("%d的阶乘是: %d\n", val,mult);
return 0;
}
//运行结果:请输入您需要求阶乘的数字:val=3 3的阶乘是:6
//阶乘的递归使用,思想是逆序,eg:求3的阶乘需要知道2的阶乘,把2的阶乘 成3,求2的阶乘需要知道1的阶乘,然后倒过来
# include
//假定n的值是1或大于1的值
long f(long n)
{
if (1 == n)
return 1;
else
return f(n - 1) * n;
}
int main(void)
{
printf("%ld\n", f(5));
//在vc中整型和长整型占的资源数一样,以为C语言只规定了整型不能比长整型大,短整型不能比整型大
//所以值也不可以写太大
return 0;
}
//运行结果:120
//1+2+3+...+100的和用递归来实现
#include
long sum(int n)
{
//用递归实现
if (1 == n)
return 1;
else
return n + sum(n - 1);
/*
//用for循环实现
long s = 0;
int i;
for (i=1; i<=n; i++)
s += i;
return s;
*/
}
int main(void)
{
printf("%ld\n", sum(100));
return 0;
}
//运行结果:5050
伪算法:
if(n > 1)
{
先将A柱子上的前n-1个盘子从A借助C移到B
将A柱子上的第n个盘子直接移到C
再将B柱子上的n-1个盘子借助A移到C
}
//汉诺塔
#include
void hannuota(int n, char A, char B, char C)
{
/* 如果是个1盘子
直接将A柱子上的盘子从A移到C
否则
先将A柱子上的n - 1个盘子借助C移到B
直接将A柱子上的盘子从A移到C
最后将B柱子上的n-1个盘子借助A移到C
*/
if (1 == n)
{
printf("将编号为%d的盘子直接从%c柱子移到%c柱子\n", n, A, C);
}
else
{
hannuota(n - 1, A, C, B); //第一个参数是放盘子的柱子,第二个参数是做为中间载体的柱子,第三个参数是要到达的柱子
printf("将编号为%d的盘子直接从%c柱子移到%c柱子\n", n, A, C);
hannuota(n - 1, B, A, C);
}
}
int main(void)
{
char ch1 = 'A'; //定义A、B、C三个柱子
char ch2 = 'B';
char ch3 = 'C';
int n;
printf("请输入要移动盘子的个数:");
scanf("%d", &n);
hannuota(n, 'A', 'B', 'C');
return 0;
}
树和森林就是以递归的方式定义的
树和图的很多算法都是以递归的方式定义的
很多数学公式就是以递归的方式定义的
斐波拉契序列:
1 2 3 5 8 13 21 34
有且只有一个称为根的节点
有若干个互不相交的子树,这些子树本身也是一颗树
树是由节点和边组成(边的功能是指向和连接节点)
每个节点只有一个父节点但可以有多个子节点
但有一个节点例外,该节点没有父节点,此节点称为根节点
节点,父节点,子节点
子孙,堂兄弟
深度:从根节点到最底层节点的层数称之为深度,根节点是第一层(树中节点的最大层次称为树的深度)
叶子节点:没有子节点的节点
非终端节点:实际就是非叶子节点
度:子节点的个数称为度
一般树:任意一个节点的子节点的个数都不受限制
二叉树:任意一个节点的子节点个数最多两个,且子节点的位置不可更改
二叉树分类:
一般二叉树
满二叉树:在不增加树层数的前提下,无法再多添加一个节点的二叉树就是满二叉树
完全二叉树:如果只是删除了满二叉树最底层最右边的连续若干个节点,这样形成的二叉树就是完全二叉树。(满二叉树是完全二叉树的一个特例)
森林:n个互不相交的树的集合
二叉树的存储
连续存储[完全二叉树]
优点:查找某个节点的父节点和子节点(也包括判断有没有子节点)
缺点:耗用内存空间过大
链式存储
一般树的存储
双亲表示法:求父节点方便
孩子表示法:求子节点方便
双亲孩子表示法:求父节点和子节点都很方便
二叉树表示法:把一个普通树转化成二叉树来存储
二叉树具体转换方法:
设法保证任意一个节点的左指针域指向它的第一个孩子,右指针域指向它的兄弟,只要满足此条件,就可以把一个普通树转化为二叉树。
一个普通树转化成的二叉树一定没有右子树
森林的存储
先把森林转化为二叉树,再存储二叉树:
将相邻的父节点依次作为节点的右子树再对各父节点进行转化
遍历
先序遍历【先访问根节点】
先访问根节点
再先序访问左子树
先序访问右子树
中序遍历【中间访问根节点】
中序遍历左子树
再访问根节点
再中序遍历右子树
后续遍历【最后访问根节点】
先中序遍历左子树
再中序遍历右子树
再遍历根节点
通过先序和中序或者中序和后序我们可以还原出原始二叉树,但是通过先序和后序是无法还原出原始的二叉树的;
换种说法:只有通过先序和中序或者中序和后序,我们才可以唯一的确定一个二叉树。
树是数据库中数据组织的一种重要形式
操作系统子父进程的关系本身就是一颗树
面向对象语言中类的继承关系本身就是一棵树
赫夫曼树
链式二叉树代码实现:
/*二叉树模型
A
/ \
B C
/
D
\
E
*/
# include
# include
struct BTNode
{
char data;
struct BTNode* pLchild;//p是指针 L是左 child是孩子
struct BTNode* pRchild;
};
struct BTNode* CreateBTree(void);
void PreTraverseBTree(struct BTNode* pT); //先序
void InTraverseBTree(struct BTNode* pT); //中序
void PosTraverseBTree(struct BTNode* pT); //后序
int main(void)
{
struct BTNode* pT = CreateBTree();
printf("先序遍历结果为:\n");
PreTraverseBTree(pT);
printf("\n中序遍历结果为:\n");
InTraverseBTree(pT);
printf("\n后序遍历结果为:\n");
PosTraverseBTree(pT);
return 0;
}
/*
先序遍历【先访问根节点】
先访问根节点
再先序访问左子树
先序访问右子树
*/
void PreTraverseBTree(struct BTNode* pT)
{
if (pT != NULL)
{
printf("%c\n", pT->data);
if (NULL != pT->pLchild)
{
PreTraverseBTree(pT->pLchild);
}
if (NULL != pT->pRchild)
{
PreTraverseBTree(pT->pRchild);
//pT->pLchild可以代表整个左子树
}
}
}
/*
中序遍历
后续遍历【最后访问根节点】
先中序遍历左子树
再中序遍历右子树
再遍历根节点
*/
void InTraverseBTree(struct BTNode* pT)
{
if (pT != NULL)
{
if (NULL != pT->pLchild)
{
InTraverseBTree(pT->pLchild);
}
printf("%c\n", pT->data);
if (NULL != pT->pRchild)
{
InTraverseBTree(pT->pRchild);
//pT->pLchild可以代表整个左子树
}
}
}
/*
后续遍历【最后访问根节点】
先中序遍历左子树
再中序遍历右子树
再遍历根节点
*/
void PosTraverseBTree(struct BTNode* pT)
{
if (pT != NULL)
{
if (NULL != pT->pLchild)
{
PosTraverseBTree(pT->pLchild);
}
if (NULL != pT->pRchild)
{
PosTraverseBTree(pT->pRchild);
//pT->pLchild可以代表整个左子树
}
printf("%c\n", pT->data);
}
}
struct BTNode* CreateBTree(void)
{
struct BTNode* pA = (struct BTNode*)malloc(sizeof(struct BTNode));
struct BTNode* pB = (struct BTNode*)malloc(sizeof(struct BTNode));
struct BTNode* pC = (struct BTNode*)malloc(sizeof(struct BTNode));
struct BTNode* pD = (struct BTNode*)malloc(sizeof(struct BTNode));
struct BTNode* pE = (struct BTNode*)malloc(sizeof(struct BTNode));
pA->data = 'A';
pB->data = 'B';
pC->data = 'C';
pD->data = 'D';
pE->data = 'E';
pA->pLchild = pB;
pA->pRchild = pC;
pB->pLchild = pB->pRchild = NULL;
pC->pLchild = pD;
pC->pRchild = NULL;
pD->pLchild = NULL;
pD->pRchild = pE;
pE->pLchild = pE->pRchild = NULL;
return pA;
}
/*
先序通历结果为:A B C D E
中序通历结果为:B A D E C
后序遍历结果为:B E D C A
*/
正在学习中
冒泡
插入
选择
快速排序
归并排序
直接选择排序
//直接选择排序排序
#include
void sort(int* a, int len)
{
int i, j, min, t;
for (i = 0; i < len - 1; ++i)
{
for (min = i, j = i + 1; j < len; ++j)
{
if (a[min] > a[j])
{
min = j;
}
}
if (min != i)
{
t = a[i];
a[i] = a[min];
a[min] = t;
}
}
}
int main(void)
{
int a[6] = { 4,0,3,2,5,1 };
sort(a, 6);
for (int i = 0; i < 6; ++i)
printf("%d\n", a[i]);
return 0;
}
//运行结果:0 1 2 3 4 5
快速排序
//快速排序
#include
int FindPos(int* a, int low, int high);
void QuickSort(int* a, int low, int high);
int main(void)
{
int a[6] = { -2,1,0,5,4,3 };
int i;
QuickSort(a, 0, 5); //第二个参数表示第一个元素的下标,第三个参数表示最后一个下标
for (i = 0; i < 6; ++i)
printf("%d ", a[i]);
printf("\n");
return 0;
}
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];
}
//终止while循环之后low和high一定是相等的
a[low] = val;
return high; //high可以改为low,但不能改为val,a[low],a[high]
}
//运行结果:-2 0 1 3 4 5
看一个程序的三步:流程,每个语句的功能,找个数一代
数据结构研究是数据的存储和数据的操作的一门学问
数据的存储分为两部分:
个体的存储
个体关系的存储
从某个角度而言,数据的存储最核心的就是个体关系的存储,个体的存储可以忽略不计
同一种逻辑结构,无论该逻辑结构物理存储是什么样子的,我们都可以它执行相同的操作
ps:第一次写,文本编辑器也不太熟悉,如果有错误,或者改进的意见,愿意通过邮箱,QQ联系我的话,本人将万分感谢。
pps:关于排序,树和图,比较复杂自己还没有理解明白,所以没有记录太多。
ppps:希望一起努力!