n 果然我还是不适合搞什么学习。。。也不是说不适合,不想搞学习,打了三个月工,别说学习了,我啥也没干好像,不知道这些能坚持多久吧,希望久一点。 这次看的是郝斌的数据结构入门,我应该就是计算机小白,废话不多说开干,笔记应该会很混乱,我是给我自己看的。
数据结构
- 数据结构=个体的存储+个体的关系存储
- 算法=对存储数据的操作
预备知识
-
衡量算法的标准
-
时间复杂度=大概程序要执行的次数,而非执行的时间
-
空间复杂度=算法执行过程中大概所占用的最大内存
-
难易程度
-
健壮性
-
-
数据结构的地位
-
数据结构是软件中最核心的课程
-
程序=数据的存储+数据的操作+可以被计算机执行的语言
-
指针和数组
指针
-
指针的重要性:指针是C语言的灵魂
-
定义:
- 地址:内存单元的编号,从0开始的非负整数
- 指针:指针就是地址,地址就是指针;指针变量是存放内存单元地址的变量;指针的本质是一个操作受限的非负整数。指针变量也是变量,只不过它存放的不能是内存单元的内容,只能存放内存单元的地址,普通变量前是不能加*,常量和表达式前不能加&。
int i=10;
int *p=&i;//等价于int *p; p=&i;
-
如何通过被调函数修改主调函数中普通变量的值
- 实参为相关变量的地址
- 形参为以改变量的类型为类型的指针变量
- 在被调函数中通过 *形参变量名的方式就可以修改主调函数的值
#include<stdio.h>
void f(int i)
{
i=100;
}
int main(void)
{
int i=9;
f(i);
printf("i=%d\n",i); //f(i)函数调用 把i的值发送给形参i(i=100) 主程序 i(i=9)输出
return 0;
}
i=9
#include<stdio.h>
void f(int *p)//不是定义了一个名字叫*p的形参,而是定义了一个形参,该形参的名字叫p,它的名字叫做int *
{
*p=100;//形参变量名
}
int main(void)
{
int i=9;
f(&i);// 实参地址 将i的地址发送给p ,*p就指向i,100赋值给i,i=100
printf("i=%d\n",i);
}
i=100
数组
-
数组名:一维数组名是个指针变量,它存放的是一维数组第一个元素的地址,它的值不能被改变,一维数组名指向的是数组的第一个元素。
- 如:a[3]==(3+a); 3[a] ==(a+3)==*(3+a);a指向第一个元素 a+3就指向第四个元素
一个整型占4个字节
#include<stdio.h>
int main(void)
{
int a[5]={1,2,3,4,5};//a就是第一个元素的地址 a指向a0 a存放的第一个元素的地址
printf("%p\n",a+1);
printf("%p\n",a+2);
printf("%p\n",a+3);//若是*a+3 则输出4 因为*a+3等价于 a[0]+3
return 0;
}
一个整型占4个字节
0019FF20
0019FF24
0019FF28
- 如何通过被调函数修改主调函数中一维数组的内容
- 被调函数接收主调函数第一个元素的地址和整个数组的长度 p[i]就是主函数的a[i]
#include<stdio.h>
void Show_Array(int *p,int len)
{
p[0]=-1; //p[0]==*(p+0)==*p 则a[0]=-1
//若p[2]=-1,则p[2]==*(p+2)==*(a+2)==a[2]=-1 只能用p变量 不能用a变量
}
int main(void)
{
int a[5]={1,2,3,4,5};
Show_Array(a,5);//a 数组首元素地址确定第一个元素 5 数组长度 元素个数
//a等价于&a[0],&a[0]本身就是 int *类型
printf("%d\n",a[0]);
return 0;
}
输出数组
#include<stdio.h>
void Show_Array(int *p,int len)
{
int i=0;
for(i=0;i<len;++i)
printf("%d\n",p[i]);
}
int main(void)
{
int a[5]={1,2,3,4,5};
Show_Array(a,5);
return 0;
}
1
2
3
4
5
要想通过一个函数访问另外一个函数的数组,只需要知道这个数组的首地址和长度。
- 所有的指针变量只占4个子节 用第一个字节的地址表示整个变量的地址
#include<stdio.h>
int main(void)
{
double *p;
double x=66.6;
p=&x; //x占8个字节,1个字节是8位,一个字节一个地址,p内只存放了一个地址,通常是字节的首地址
double arr[3]={1.1,2.2,3.3};
double *q;
q=&arr[0];
printf("%p\n",q);//%p指以十六进制输出
q=&arr[1];
printf("%p\n",q);
return 0;
所示结果:
0019FF0C
0019FF14 arr[0]与arr[1]差8位 16进制逢16进1
}
- 如何通过函数改变实参的值
#include<stdio.h>
void f(int **q);
int main(void)
{
int i=9;
int *p=&i;//等价于 int *p; p=&i;
printf("%p\n",p);
f(&p);
printf("%p\n",p);
return 0;
}
void f(int ** q)//p是 int *类型 取p的地址就是int **类型(指针的指针)
{
*q=(int *)0xFFFFFFFF;//修改指针变量的值只能写它的地址
}
所示结果:
0019FF2C
FFFFFFFF
结构体
-
什么叫结构体
-结构体是用户根据实际需要自己定义的复合数据类型
①通过结构体变量名.来实现 st.sid
#include<stdio.h>
struct Student
{
int sid;
char name[200];
int age;
};//分号不能省,结构体不是变量
int main(void)
{
struct Student st={1000,"wjq",21};//st结构体变量名
printf("%d %s %d\n",st.sid,st.name,st.age);//要想访问结构体的成员必须通过结构体变量名.(st.)的方式来使用
//另一种赋值方法
// st.sid=1000;
// st.name="wjq"; 这种方式是错误的
// strcpy(st.age,"wjq"); 首处还需要添加include<string.h>
// st.age=21
return 0;
}
所示结果:
1000 wjq 21
② 通过指向结构体变量的指针来实现 (重点) pst->sid
int main(void)
{
struct Student *pst; //定义一个指针变量叫pst,存放的叫struct Student这个变量的地址
pst=&st; //pst存放st的地址
pst->sid=99; //pst->sid 等价于 (*pst).sid,而(*pst)等价于st,则pst->sid等价于st.sid
return 0;
}
pst->sid(pst所指向的结构体变量中的sid这个成员)
-
注意事项
- 结构体变量不能加减乘除,但可以相互赋值(=)
- 结构体变量和结构体指针变量作为函数传参的问题
动态分配内存概(malloc函数)
-
动态分配的优点
①在程序运行的过程中根据用户不同的需要动态的构造不同长度的数组
②在程序运行的过程中释放
#include<stdio.h>
#include<malloc.h>
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);//int占4个字节 * len(5个长度)=20个字节
//(int *) 强制转化为int类型的指针变量,因为malloc函数默认返回void类型,需要类型转化
//molloc函数只能返回第一个字节的地址,第一个字节地址无实际含义(干地址)
*pArr=4;//类似于a[0]=4;
pArr[1]=10;//类似于a[1]=10;
printf("%d%d\n",*pArr,pArr[1]);
free(pArr);//把pArr所代表的动态分配的20个字节的内存释放
return 0;
}
所示结果:
请输入你需要分配数组的长度;len=5
4 10
跨函数使用内存
- 例题(假设在一个函数f里面分配了一块内存,在另外一个函数g中调用该f函数,那么在g函数中是否可以使用f函数中分配的内存)
#include<stdio.h>
int f();
int main(void)
{int i=10;
i=f();
printf(“i=%d\n”,i);
for(i=0;i<2000;++i)
f();
return 0;
}
int f()
{
int j=20;
return j;
}
#include<stdio.h>
#include<malloc.h>
struct Student
{
int sid;
int age;
};
struct Student *CreateStudent(void);
void ShowStudent(struct Student *);
int main(void)
{
struct Student *ps;//ps为指针变量 指向4个字节
//struct Student st;
//结构体struct Student中有两个指向4个字节的int类型(内存对齐) st占8个字节、
//如果是一个int一个char类型 char字节小于int字节 会按照2个int字节相加
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=20;
return p;
}
所示结果:
99 20
线性结构
- 把所有的结点(元素或个体)用一根直线穿起来
连续存储【数组】
- 什么叫数组:元素类型相同,大小相等
#include<stdio.h>
#include<malloc.h>
#include<stdlib.h> // 包含了exit函数
struct Arr
{
int *pBase;//存储的是数组第一个元素的地址
int len;//数组所能容纳的最大元素的个数
int cnt;//当前数组有效元素的个数
};//结构体是数据类型,不是变量,该数据类型的名字叫struct Arr,该数据类型含有三个成员。
void init_arr(struct Arr *pArr,int length);//初始化,使pBase指向一个有效的数组,而不再是垃圾数字
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);//删除,用指针接收
intget();//获取下标为某个的值
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 innversion_arr(struct Arr *pArr);//倒置
int main(void)
{
struct Arr arr;//定义了一个变量arr,已经分配内存了,但是只定义未初始化,内部的三个变量都是垃圾数字
int val;
init_arr(&arr,6);
show_arr(&arr);
append_arr(&arr,1);
append_arr(&arr,2);
append_arr(&arr,3);
append_arr(&arr,4);
append_arr(&arr,5);
insert_arr(&arr,1,99);//在1 的位置上插入99
if(delete_arr(&arr,1,&val)) //删除 取地址,数值,把val地址发给pVal,在该函数内部可以通过pVal修改val的值
{
printf("删除成功!\n");
printf("您删除的元素是:%d\n",val);
}
else
{
printf("删除失败!\n");
}
/* append_arr(&arr,6);
append_arr(&arr,7);
if( append_arr(&arr,8)
{
printf("追加成功!\n");
}
else
{
printf("追加失败!\n");
}
*/
show_arr(&arr);
innversion_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)//结构体变量可以相互赋值 把arr的值赋值给array
{
*pArr;//相当于 arr 因为把arr的地址赋值给了pArr
//array.len=99;
//结构体Arr 中三个成员最少占12个字节
//结构体变量arr赋值给另一个结构体变量array
//该变量属于函数init_arr,len=99赋值给形参为array分配的内存对实参为arr无影响,arr中依旧是垃圾数字,不会改变
(*pArr).len=99;//改变了
pArr->pBase=(int *)malloc(sizeof(int)*length);//指针变量pArr指向结构体中的pBase成员
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(数组为空)
// 提示用户数组为空
// else
// 输出数组有效内容
// 以上为伪算法
if(is_empty(pArr))
//接收 struct Arr *类型的地址 应该为结构体变量的地址 pArr本身就是 struct Arr *类型就是结构体变量地址
{
printf("数组为空!\n");
}
else
{
for(int i=0;i<pArr->cnt;++i)
printf("%d",pArr->pBase[i]);//pArr指向的主函数中结构体变量中的pBase成员
}
}
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++;//个数+1
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)//pos就是指的第几个数
{
pArr->pBase[i-1]=pArr->pBase[i];
}
pArr->cnt--;//删除后个数-1
return true;
}
//假设让a,b的值互换 中介值t t=a; a=b;b=t;
void innversion_arr(struct Arr *pArr)//倒置
{
int i;
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;i<pArr->cnt;++j)
{
if(pArr->pBase[i]>pArr->pBase[j])
{
t=pArr->pBase[i];
pArr->pBase[i]=pArr->pBase[j];
pArr->pBase[j]=t;
}
}
}
}
离散存储【链表】
-
定义:
- n个节点离散分配
- 彼此通过指针相连
- 每个节点只有一个前驱节点,每个节点只有一个后续节点。
- 首节点没有前驱节点,尾节点没有后续节点
-
专业术语:首节点(第一个有效节点),尾结点(最后一个有效节点),头结点(第一个有效节点之前的节点,方便确认首节点的位置,并不存放有效数据,数据类型和首节点类型一样),头指针(指向头结点的指针变量,存放了头结点的地址),尾指针(指向尾结点的指针)
-
如果希望一个函数来对链表进行处理,我们至少需要接受链表的那些参数:只需要一个参数(头指针),因为我们通过头指针可以推算出链表的其他所有信息。
-
分类:
-
单链表
-
双链表:每一个节点有两个指针域
-
循环链表:能通过任何一个节点找到其他所有的节点
-
非循环链表:
-
-
算法:遍历,查找,清空,销毁,求长度,排序,删除节点,插入节点
- 狭义的算法是与数据的存储方式密切相关的
- 广义的算法是与数据的存储方式无关的
- 泛型:利用某种技术达到的效果就是:不同的存储方式,执行的操作是一样的
-
链表优缺点:
typdef的用法
#include<stdio.h>
//typedef int WANG;//为int重新再多取一个名字,WANG等价于int
typedef struct Student
{
int sid;
char name[100];
char sex;
}* PSTU,STU; //PSTU等价于struct Student *类型,PST是一个指针。 STU等价于struct Student
int main(void)
{
/* int i=10;//等价于WANG i=10;
WANG j=20;
printf("%d\n",j);
struct Student st;//等价于ST st;
struct Student *ps=&st;//等价于ST*ps
ST st2;//ST 取代了struct Student
st2.sid=200;
printf("%d\n",st2.sid);
*/
STU st;//等价于struct Student st;
PSTU ps=&st;//等价于struct Student *ps=&st;
ps->sid=98;
printf("%d\n",ps->sid);
return 0;
}
所示结果:
98
每一个链表节点的数据类型如何表示
#include<stdio.h>
//每一个节点都用过数据类型来表示
//一个结构体变量中的成员指向它本身
struct Node
{
int data;//数据域
struct Node *pNext;//指针域
};NODE,*PNODE//NODE等价于 struct Node,PNODE等价于struct Node *
int main(void)
{
return 0;
}
非循环链表插入节点伪算法
p没有指针域,p是指针变量,p指向的结构体变量才有指针域
q里面存放了节点本身的地址,q->pNext存放了下一个节点的地址,q->pNext=p->pNext把p所指向的下一个节点赋值给了q指向的下一个节点,则q就指向了p的下一个节点
插入方法一:r=p->pNext;p->pNext=q; q->pNext=r;
插入方法二:q->pNext=p->pNext;p->pNext=q;
非循环链表删除节点伪算法
p=pNext->pNext 表示第二个节点指针域,第二个节点的指针域存放最后一个节点的地址
方法一:p->pNext=p=pNext->pNext;//不推荐!!! 导致内存泄漏没有释放内存
方法二:先临时定义一个指向p后面节点的指针r
r=p->pNext;//r指向p后面的那个结点
p->pNext=p->pNext->pNext;
free(r);
链表创建和链表遍历
#include<stdio.h>
#include<malloc.h>
#include<stdlib.h>
//每一个节点都用过数据类型来表示
//一个结构体变量中的成员指向它本身
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 pHead);//链表长度
bool insert_list(PNODE,int,int);//(链表,那个位置插入,插入的值)
bool delete_list(PNODE,int,int *);//(链表,删除哪一个节点,删除节点存放)
void sort_list(PNODE);//排序
int main(void)
{
PNODE pHead=NULL;//等价于 struct Node *=NULL;
pHead=create_list();//create_list()功能:创建一个非循环单链表,并将该链表的头结点赋给pHead
traverse_list(pHead);
//判断为空
/* if(is_empty(pHead))
printf("链表为空!\n");
else
printf("链表不空!\n");
*/
//链表长度
int len=length_list(PNODE);
printf("链表的长度是:%d\n",len);
sort_list(pHead);
return 0;
}
PNODE create_list(void)//函数需要返回一个结构体定义节点的地址,PNODE就是typedef定义的存放结构体地址的指针,所以函数类型为PNODE
{
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)//循环len次 每一次都分配一个新的节点 用pNew表示这个新的节点
{
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;//指向尾结点
//用pNew生成一个临时的节点,把临时节点存放val,把临时节点pNew挂到了pTail->pNext,把pNew这个临时节点的指针域清空,pNew指向pTail
}
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;//p指向第一个有效节点
int len=0;
while (NULL!=p)
{
++len;
p=p->pNext;//p不为空,len+1,p的位置后移一位
}
return len;
}
void sort_list(PNODE pHead)//排序
{
int i,j,t;
int len=length_list(pHead);
PNODE p,q; // int i,j,t;
for(i=0,p=pHead->pNext;i<len-1;++i,p=p->pNext)//for(i=0;i<len-1;++i)i=0从数组表示等于第一个有效元素的下标,p表示第一个有效元素的地址,p=p->pNext(++i)
{
for(j=i+1,q=p->pNext;j<len;++j,q=q->pNext)//for(i=i+1;j<len;++j)
{
if(p->data>q->data)// if(a[i]<>a[j])
{
t=p->data;//t=a[i];
p->data=q->data;//a[i]=a[j]
p->data=t;//a[i]=t
}
}
}
}
所示结果:
请输入您需要生成的链表节点的个数:len=3
请输入第1个节点的值:1
请输入第2个节点的值:2
请输入第3个节点的值:3
123
复习
数据结构:
- 狭义
- 数据结构:数据结构是专门研究数据存储的问题
- 数据的存储包含两方面:个人的存储和个体关系的存储
- 算法是对存储数据的操作
- 广义
- 数据结构既包含数据的存储也包含数据的操作,对存储数据的操作就是算法 算法:
- 狭义
- 算法是和数据的存储方式密切相关
- 广义
- 算法和数据的存储方式无关(泛型思想) 数据的存储结构:
- 线性
-
连续存储【数组】
优点:存取速度快 缺点:插入删除元素慢效率低、空间通常有限制、事先必须知道数组的长度、需要大块连续的内存块 -
离散存储【链表】
优点:空间无限制、插入删除元素快 缺点:存取速度慢 -
线性结构的应用--栈&队列
-
- 非线性
- 树
- 图
线性结构的应用【栈】
#include<stdio.h>
#include<malloc.h>
void f(int k)
{
int m;
double *q=(double *)malloc(200)
}
int main(void)
{
int i=10;
int *p=(int*)malloc(100);
return 0;
}
//其中m,q,i,p是在栈中分配的(静态)
// 200,100在堆中分配(动态)
- 栈
- 一种可以实现‘先进后出’的存储结构
- 分类:静态栈&动态栈
- 算法:出栈&入栈
#include<stdio.h>
#include<malloc.h>
#include<stdlib.h>
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("出栈失败");
}
return 0;
}
void init(PSTACK pS)//pS保存S的地址
{
pS->pTop=(PNODE)malloc(sizeof(NODE));//创建栈节点,头栈节点
if(NULL==pS->pTop)
{
printf("动态内存分配失败!\n");
exit(-1);
}
else
{
pS->pBottom=pS->pTop;//栈顶和栈底都指向指针域为空的头栈节点
pS->pTop->pNext=NULL;//pS->Bottom->pNext=NULL; 头栈节点的指针域为空
}
}
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;
}
void push(PSTACK pS,int val)
{
PNODE pNew=(PNODE)malloc(sizeof(NODE));//创建新节点
pNew->data=val;//给栈节点的数据域赋值
pNew->pNext=pS->pTop;//栈节点指针域保存下一个节点的地址
pS->pTop=pNew;
}
//把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;//p指向栈顶
PNODE q=NULL;
while(p!=pS->pBottom)//只要p没有指向栈底元素(有效元素) 释放p
{
q=p->pNext;//q指向栈顶元素的下一层元素
free(p);
p=q;
}
pS->pTop=pS->pBottom;
}
}
所示结果:
654321
出栈成功,出栈的元素是6
CLEAR后
654321
出栈失败
-
应用
- 中断
- 表达式求值
- 内存分配
- 缓冲处理
- 迷宫
- 函数调用 f()函数中调用g()函数,g()函数中调用k()函数
在f函数的执行过程中调用g函数:g函数后有其他语句,调用g函数之后的地址和调用g函数所分配产生的变量(局部变量)。把g函数调用完后的语句的地址以及为g函数所分配的函数以及所有变量和其他相关信息压入一个栈,在栈中执行。K同理,语句执行完就是只被分配的空间释放--出栈,出栈后分配下一个语句的地址
线性结构的应用【队列】
-
队列
-
定义:一种可以实现“先进先出”的存储结构
-
分类
-
链式队列:用链表实现
-
静态队列:用数组实现 (静态队列通常都必须是循环队列)
-
-
循环队列
1.静态队列为什么必须是循环队列?
-
2.循环队列需要几个参数来确定?
2个参数:front和rear
- 循环队列各个参数的含义?
(1)队列初始化fron和rear的值都是0.
(2)队列非空:front代表的是队列的第一个元素,rear代表的是队列的最后一个有效元素的下一个元素。
(3)队列为空:front和rear的值相等,但不一定为0。
- 循环队列入队伪算法
3. 循环队列出队伪算法
f=(f+1)%数组的长度
- 如何判断循环队列为空
如果front和rear的值相等,则该队列就一定为空
5.如果判断循环队列已满 预备知识:front的值可能比rear大,也可能比rear小,当然也可能相等
(1)多增加一个标识参数
(2)少用一个元素(常用方式)
-
队列算法
- 入队
- 出队
#include<stdio.h>
#include<malloc.>
typedef struct 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;
init(&Q);
int val;
en_queue(&Q,1);
en_queue(&Q,2);
en_queue(&Q,3);
en_queue(&Q,4);
en_queue(&Q,5);
en_queue(&Q,6);
traverse_queue(&Q);
if(out_queue(&Q,&val))
{
printf("出队成功,队列出队的元素是:%d\n",val);
}
else
{
printf("出队失败");
}
traverse_queue(&Q);
return 0;
}
void init(QUEUE *pQ)//pQ存放的Q 的地址 *pQ就是Q
{
pQ->pBase=(int *)malloc(sizeof(int)*6);//pBase指向了一个数组(6个元素 24个字节 每四个字节划分) pBase代表数组第一个元素的地址
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;
}
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.不同函数之间的相互调用
#include<stdio.h>
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
2.自己调用自己
#include<stdio.h>
void f( int n)
{
if(n==1)
printf("保持开心\n");
else
f(n-1);
}
int main(void)
{
f(3);
return 0;
}
所示结果:
保持开心
- 举例
求阶乘
①循环
#include<stdio.h>
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
}
②递归
#include<stdio.h>
//假定n的值是1或者大于1的值
long f(long n)
{
if(1==n)
return 1;
else
return f(n-1)*n;//f是求n的阶乘 n等于1 阶乘就是1 只需要知道n-1的阶乘
}
int main(void)
{
printf("%d\n",f(3));
return 0;
}
所示结果:6
1+2+3+...+100的和
#include<stdio.h>
long sum(int n)
{
if(1==n)
return 1;
else
return n+sum(n-1);
}
int main(void)
{
printf("1+2+3+...+100=%ld\n",sum(100));
return 0;
}
所示结果:
1+2+3+...+100=5050
- 间接调用自己
#include<stdio.h>
void f(int n)
{
g(n);
}
void g(int m)
{
f(m);
}
//间接调用 :f调用g ,g调用f本身
int main(void)
{
return 0;
}#include<stdio.h>
void f(int n)
{
g(n);
}
void g(int m)
{
f(m);
}
//间接调用 :f调用g ,g调用f本身
int main(void)
{
return 0;
}
-
一个函数为什么可以调用自己
//一个函数为什么可以调用自己(A函数调用B函数)
#include <stdio.h>
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;
}
//递归的值可以递增,但处理数据的规模在递减
#include <stdio.h>
int g(int);//前缀声明
int f(int n)
{
if(n>7)
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);
printf("val=%d\n",val);
return 0;
}
所示结果:
哈哈
val=8
-
递归满足的三个条件
- 1.递归必须得有一个明确的终止条件。
- 2.该函数所处理的数据规模必须在递减。
- 3.这个转化必须是可解的。
-
循环和递归
-
递归:
-
优点:易于理解
-
缺点:速度慢、储存空间大
-
-
循环
- 优点:速度快、存储空间小
- 缺点:不易理解
-
-
递归的应用
- 树和森林就是以递归的方式定义的
- 树和图的很多算法都是以递归实现的
- 很多数学公式就是以递归的方式定义的
非线性结构【树】
-
树
- 定义
-
1.有且只有一个称为根的节点
-
2.有若干个互不相交的子树,这些子树本身也是一颗树。
-
专业定义
- 1.树是由节点和边组成
- 2.每个节点只有一个父节点但是可以有多个子节点
- 3.但只有一个节点除外,该节点没有父节点,该节点为根节点
-
专业术语:节点、父节点、子节点、子孙、堂兄弟。
- 深度:从根节点到最底层节点的层数
- 叶子节点:没有子节点的节点
- 非终端节点:实际就是非叶子节点
- 度:子节点的个数
-
- 定义
-
分类
-
一般树:任意一个节点的子节点的个数都不受限制。
-
二叉树:任意一个节点的子节点个数最多两个,且子节点的位置不可更改
-
分类:
- 一般二叉树
- 满二叉树:在不增加树层数的前提下,无法再多添加一个节点的二叉树
- 完全二叉树:只删除满二叉最底层最右边的连续若干节点
-
-
森林:n个互不相交的树的集合
-
-
树的存储
-
二叉树的存储
-
连续存储【完全二叉树】
-
链式存储
- 优点:查找某个节点的父节点和子节点(也包括判断有没有子节点)
- 缺点:耗用内存空间过大
-
-
一般树的存储
-
双亲表示法
- 求父节点方便
-
孩子表示法
- 求子节点方便
-
双亲孩子表示法
- 求父节点和子节点都方便
-
二叉树表示法
-
把一个普通树转化为二叉树来存储
设法保证任意一个节点的左指针域指向它的第一个孩子,右指针域指向它的堂兄弟,只要能满足此条件,就可以把一个普通树转化为二叉树。 一个普通树转化成的二叉树一定没有右子树。
-
-
-
双亲表示法:
孩子表示法:
孩子双亲表示法:
-
森林的存储
-
森林转化为二叉树
- (1)先把每棵树转换为二叉树;
- (2)兄弟放右边,孩子放左边
-
- 二叉树的操作
- 遍历
-
先序遍历:根左右
-
中序遍历:左根右
-
后序遍历:左右根
-
- 已知两种遍历序列求原始二叉树
-
通过先中序、中后序可以还原出原始二叉树,但是通过先后序是无法唯一还原出原始的二叉树的。
-
- 遍历
- 树的应用
- 树是数据库中数据组织的一种重要形式
- 操作系统子父进程的关系本身就是一颗树
- 面向对象语言中类的继承关系本身就是一棵树
- 链式二叉树的遍历
#include<stdio.h>
#include<malloc.h>
struct BTNode
{
int data;
struct BTNode *pLchhid;//p是指针,L是左,child是孩子
struct BTNode *pRchhid;
};
void PreTraverseBTree( struct BTNode * pT);
void InTraverseBTree( struct BTNode * pT);
void PostTraverseBTree( struct BTNode * pT);
struct BTNode * CreateBTree(void);
int main(void)
{
struct BTNode * pT=CreateBTree();
printf("前序遍历:\n");
PreTraverseBTree(pT);
printf("中序遍历:\n");
InTraverseBTree(pT);
printf("后序遍历:\n");
PostTraverseBTree(pT);
return 0;
}
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->pLchhid=pB;
pA->pRchhid=pC;
pB->pLchhid=pB->pRchhid=NULL;
pC->pLchhid=pD;
pC->pRchhid=NULL;
pD->pLchhid=NULL;
pD->pRchhid=pE;
pE->pLchhid=pE->pRchhid=NULL;
return pA;
}
void PreTraverseBTree( struct BTNode * pT)
{
if(pT!=NULL)
{
printf("%c\n",pT->data);
if(NULL!=pT->pLchhid)
{
PreTraverseBTree(pT->pLchhid);
}
if(NULL!=pT->pLchhid)
{
PreTraverseBTree(pT->pRchhid);
}
//pT->pLchhid可以代表整个左子树
}
}
void InTraverseBTree( struct BTNode * pT)//中序 左根右
{
if(pT!=NULL)
{
if(NULL!=pT->pLchhid)
{
InTraverseBTree(pT->pLchhid);
}
printf("%c\n",pT->data);
if(NULL!=pT->pLchhid)
{
InTraverseBTree(pT->pRchhid);
}
//pT->pLchhid可以代表整个左子树
}
}
void PostTraverseBTree( struct BTNode * pT)//后序:左右根
{
if(pT!=NULL)
{
if(NULL!=pT->pLchhid)
{
PostTraverseBTree(pT->pLchhid);
}
if(NULL!=pT->pLchhid)
{
PostTraverseBTree(pT->pRchhid);
}
printf("%c\n",pT->data);
//pT->pLchhid可以代表整个左子树
}
}
所示结果:
前序遍历:
A
B
C
D
中序遍历:
B
A
D
C
后序遍历:
B
D
C
A
Press any key to continue
图
- 图的定义和术语
- G=(V,E)
- V:顶点(数据元素)的有穷非空集合;
- E:边的有穷集合
- 无向图:每条边都是无方向的
- 有向图:每条边都是有方向的
- 完全图:任意两个点都有一条边相连
- 稀疏图:有很少边或弧的图(e<nlogn)
- 稠密图:边/弧带权的图
- 顶点的度:与该顶点相关联的边的数目,记作TD(v)
- 路径:接续的边构成的顶点序列
- 路径长度:路径上边或弧的数目/权值之和
- 回路(环):第一个顶点和最后一个顶点相同的路径
- 简单路径:除路径起点和终点可以相同外,其余顶点均不相同的路径
- 简单回路(简单环):除路径起点和终点相同外,其余顶点均不相同的路径
- 连通图(强连通图):在无(有)向图G=(V,{E})中,若对任何两个顶点v,u都存在从v到u的路劲,则称G是连通图(强连通图)
- 权:图中边或弧所具有的的相关数称为权。表明从一个顶点到另一个顶点的距离或耗费
- 网:带权的图
- 子图:设有两个图G=(V,{E})、G1=(V1,{E1}),若V1属于V、E1属于E,则称G1是G的子图。
- (强)连通分量:无向图G的极大连通子图称为G的连通分量。
- 极大连通子图:该子图是G连通子图,将G的任何不在该子图中的顶点加入,子图不再连通
- 极小连通子图:该子图所示G的连通子图,在该子图中删除任何一条边,子图不再连通。
- 生成树:包含无向图G所有顶点的极小连通子图。
- 生成森林:对非连通图,由各个连通分量的生成树的集合。 无向图:
有向图:
完全图:(左边:无向完全图 右边:有向完全图) 无向:n个顶点 n(n-1)/2条边
有向:n个顶点 n(n-1)条边
度:
(强)连通图:
子图:
连通分量:
强连通分量:
极小连通子图&生成树:
图的存储结构
- 领接矩阵(数组表示法) 无向图:
有向图:
有权图(网):
- 领接矩阵创建算法步骤
- 输入总顶点数和总边数
- 依次输入点的信息存入顶点表中
- 初始化领接矩阵,使每个权值初始化为极大值
- 构造领接矩阵
- 链接矩阵的优缺点
- 优点:
- 直观、简单、
- 方便检查任意一对顶点间是否存在边
- 方便找任意顶点的所有‘邻接点’
- 缺点
- 不便于增加和删除顶点
- 浪费空间--存稀疏图(点很多而边很少)有大量无效元素
- 浪费时间--统计稀疏图中一共有多少条边
- 优点:
- 邻接表
- 领接表表示法(链式)
- 无向图
-
有向图
- 邻接矩阵和领接表的区别
- 对于任意确认的无向图,领接矩阵是唯一的(行列号与顶点编号一致),但领接表不唯一(链接次序与顶点编号无关)
- 领接矩阵的空间复杂度为O(n²),而领接表的空间复杂度为O(n+e)
- 领接矩阵多用于稠密图,领接表多用于稀疏图
图的遍历
- 深度优先遍历(DFS)
- 一杆子插到底,走不通就退回上一步
- 广度优先遍历(BFS)
- 访问邻接点的邻接点(类似树的层次遍历)
图的应用
- 生成树:所有顶点均由边连接在一起,但不存在回路的图
- 特点:
- 生成树的顶点个数与图的顶点个数相同
- 生成树是图的极小连通子图,去掉一条边则非连通
- 一个有n个顶点的连通图的生成树有n-1条边
- 在生成树中再加一条边必然形成回路
- 生成树中任意两个顶点间的路径是唯一的
- 特点:
- 最小生成树
- prim算法
- 从顶点出发选邻接点权值最小的边
- Kruskal算法
- 先选出最小权值的边(避免出现环路),直到连接所有点。
- prinm和kurskal算法比较
- 最短路径
- 两点间最短路径
- 用Dijkstra算法
- 某源点到其他各点最短路径
- Floyd算法
- 逐个顶点试探
- 从vi到vj的所有可能存在的路径中选出一条长度最短的路径
- Floyd算法
- 拓扑排序
- AOV网
- 若从i到j有一条有向路劲,则i是j的前驱,j是i的后继。(C1是C5的前驱,C5是C1的后继)
- 若<i,j>是网中有向边,则i是j的直接前驱,j是i的直接后继。(C1是C3的直接前驱,C3是C1的直接后继)
- 不允许有回路
- 拓扑排序的方法
- 在有向图中选一个没有前驱的顶点且输出
- 从图中删除该顶点和所有以它为尾的弧
- 重复前两步
- 应用:检测AOV网中是否存在环
- 对有向图构造其顶点的拓扑有序序列,若网中所有顶点都在它的拓扑有序序列中,则该AOV网必定不存在环
- AOV网
- 关键路径
- A0E网
- 把工程计划表示为边,表示活动的网络叫AOE网,用顶点表示事件,弧表示活动,弧的权表示活动持续时间
- 关键路径:路径长度最长的路径
- 路径长度:路径上个活动持续时间之和
- 最早发生时间:从起点开始权值相加取最大值 ve
- 最晚发生时间:从汇点开始减去前面的权值取最小 vl
- 活动最早发生时间:活动出发节点对应的最早发生时间
- 活动最晚发生时间:活动结尾节点对应的最晚发生时间-活动时间
- A0E网
排序
- 插入排序
- 直接插入排序、折半插入排序、希尔排序
- 交换排序
- 冒泡排序、快速排序
- 选择排序
- 简单选择排序、堆排序
- 归并排序
- 2路归并排序
- 基数排序
插入排序
- 在有序序列中插入一个元素,保持序列有序,有序长度不断增加。
直接插入排序
折半插入排序
希尔排序
- 先将整个待排序分割成若干个子序列,分别进行直接插入排序,待整个序列中的记录‘基本有序’时,再对全体记录进行依次直接插入排序
交换排序
- 两辆比较,如果发生逆序则交换,直到所有记录都排好序为止。
冒泡排序
- 每趟不断将记录两两相比较,并按‘前大后小’规则交换
快速排序
- 任取一个元素为中心,所以比它小的放在前面,比它大的放在后面,形成两个子表,对各个子表重新选择中心元素并依次调整,直到每个子表只剩一个元素
- 子表的形成采用后面的low high交替向中心逼近法
- 输入数据次序越乱越好
选择排序
简单选择排序
- 在待排序的数据中选出最大(小)的元素放在最终位置
- 从n个记录中找到关键字最小的记录,将其与第一个记录交换
- 再通过n-2次比较,将剩余的n-1的记录中找到关键字次小的记录,将其与第二个记录交换
- 重复上述操作,进行n-1次交换
堆排序
- 堆
- 若n个元素的序列{a1,a2...a3} { ai<=a2i ai<=a2i+1} {ai>=a2i ai>=a2i+1} 则分别称为该序列{a1,a2...an}为小根堆和大根队
- 堆的实质是完全二叉树:二叉树中任一非叶子结点均小于(大于)它的孩子结点(根大(小)于左右子树)
- 判断
- 如何在输出堆顶元素后,调整剩余元素成一个新的堆
- 小根堆
- 输出堆顶元素后,以堆中最后一个元素代替
- 将根节点的值与左右子树的根节点值比较与其较小的交换
- 大根堆
- 输出堆顶元素后,以堆中最后一个元素代替
- 将根节点的值与左右子树的根节点值比较与其较大的交换
- 小根堆
- 建堆
- 建小根堆
- 从最后一个非叶子节点开始调整,因为49为叶子节点,从n/2个节点开始调整(8/2=4)再从n/2-1的节点开始调整
- 从97开始调整,97与49比较,97>49,位置交换
- 从下标为3(65)的子树调整 65>27>13 位置与13交换
- 从下标为2(38)的子树调整 38<49<76 位置不用调整
- 从下标为1(49)的子树调整 49>38>13 位置与13交换
- 再进行剩余子树的比较 右子树根节点27比49小 位置交换
归并排序
二路归并排序
基数排序
- 分配+收集
- 也称为桶排序或箱排序:设置若干箱子,将关键字为K的记录放入第K个箱子,然后再按序号将非空的连接。