我们如何把现实中大量存在而复杂的问题以**特定的数据类型**和**特定的存储结构**保存到主存储器(内存)中,以及在此基础上为实现某个功能(例如:查找某个元素、删除某个元素,对所有元素进行排序)而执行的相应操作,这个相应的操作叫算法。
数据结构 =>个体+个体的关系
算法 => 对存储数据的操作
即解决两个问题——个体如何保存?个体与个人之间的关系是怎么保存的?
是解题的方法和步骤
1、时间复杂度——程序要执行的次数,而非执行时间
2、空间复杂度——算法执行过程中大概所占用的最大内存
3、难易程度——别人是否能看懂你的程序
4、健壮性——当程序进行一些非法输入时,程序是否会崩溃
1、算法采用的策略和方案
2、编译产生的代码质量
3、问题的输入规模
4、机器执行指令的速度
1、定义
非用运行时间去度量,而是用运行时进行的基本操作执行次数来度量
2、计算方法:大O推导法
(1)用常数 1 取代运行时间中的所有加法常数
(2)在修改后的运行次数函数中,只保留最高阶项
(3)如果最高阶项存在且不是 1,则去除与这个项相乘的常数,得到的结果就是大 O 阶
3、大O阶大小比较
(1)可以忽略加法常数
O(2n + 3) = O(2n)
(2)与最高次项相乘的常数可忽略
O(2n^2) = O(n^2)
(3) 最高次项的指数大的,函数随着 n 的增长,结果也会变得增长得更快
O(n^3) > O(n^2)
(4)判断一个算法的(时间)效率时,函数中常数和其他次要项常常可以忽略,而更应该关注主项(最高阶项)的阶数
O(2n^2) = O(n^2+3n+1)
O(n^3) > O(n^2)
1、定义
算法的空间复杂度通过计算算法所需的存储空间实现,即运行完一个程序所需内存的大小
2、空间复杂度的计算
利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计
一个算法所需的存储空间用f(n)表示
空间复杂度的计算公式记作:S(n)=O(f(n))
其中n为问题的规模
S(n)表示空间复杂度
(1)忽略常数,用O(1)表示
(2)递归算法的空间复杂度=递归深度N*每次递归所要的辅助空间
(3)对于单线程来说,递归有运行时堆栈,求的是递归最深的那一次压栈所耗费的空间的个数
因为递归最深的那一次所耗费的空间足以容纳它所有递归过程
一般情况下,一个程序在机器上执行时:
除了需要存储程序本身的指令,常数,变量和输入数据外
还需要存储对数据操作的存储单元的辅助空间
若输入数据所占空间只取决于问题本身,和算法无关
这样就只需要分析该算法在实现时所需的辅助单元即可。若算法执行时所需的辅助空间相对于输入数据量而言是个常数,则称此算法为原地工作,空间复杂度为O(1)
3、需存储的空间
1)固定部分
这部分属于静态空间
这部分空间的大小与输入/输出的数据的个数多少、数值无关
主要包括指令空间(即代码空间)、数据空间(常量、简单变量)等所占的空间
(2)可变空间
这部分空间的主要包括动态分配的空间,以及递归栈所需的空间等
这部分的空间大小与算法有关
1、算法的时间复杂度和空间复杂度是可以相互转化的
2、常用的算法的时间复杂度和空间复杂度
指针就是地址,地址就是指针。
指针的本质是一个操作受限的非负整数。(虽说是整数,但不能用来做加减乘除运算)
指针变量:存放内存单元地址的变量。
指针的重要性:表示一些复杂的数据结构
快速的传送数据
使函数返回一个以上的值
能否直接访问硬件
能够方便的使用数组和字符串
是理解面向对象语言中引用的基础
指针是C语言的灵魂。
#include
int main(void)
{
int * p;//p是个变量名字,int *表示该p变量只能存储int类型变量的地址
int i = 10;
int j;
return 0;
}
①P保存i的地址,p就指向i(*p和i是一个东西)
②修改p的值不影响i的值,修改i的值不影响p的值。
③*p 是i
一个指针变量,无论其指向的变量占几个字节,其自身都是占4个字节。(一个字节一个地址)
无论什么类型的变量,实参,返回类型又是void,若想修改其值,**参数必须是其地址**。
注意:
指针变量也是变量,只不过它存放的不能是内存单元的内容,只能存放内存单元的地址
普通变量前不能加*
常量和表达式前不能加&
#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;
}
如何通过被调函数修改主调函数中普通变量的值
Ⅰ 实参(主函数里调用时)为相关变量的地址
Ⅱ 形参为以该变量的类型为类型的指针变量
Ⅲ 在被调函数中通过 ** 形参变量名* 的方式就可以修改主函数相关变量的值
=>将实参中的地址传递给形参
指针 和 一维数组
数组名
一维数组名是个指针常量,
它存放的是一维数组第一个元素的地址,
它的值不能被改变
**一维数组名指向的是数组的第一个元素**
下标和指针的关系
a[i] <<==>> *(a+i)
#include
int main(void)
{
int a[i] = {1,2,3,4,5};
a[3] = *(3+a);
// *a+3 等价于a[0]+3
return 0;
}
假设指针变量的名字为p
则p+i的值是p+i*(p所指向的变量所占的字节数)
**指针变量的运算**
指针变量不能相加,不能相乘,不能相除
如果两指针变量属于同一数组,则可以相减
指针变量可以加减一整数,前提是最终结果不能超过指针允许指向的范围
p+i的值是p+i*(p所指向的变量所占的字节数)
p-i的值是p-i*(p所指向的变量所占的字节数)
p++ <==> p+1
p-- <==> p-1
如何通过被调函数修改主调函数中一维数组的内容【如何界定一维数组】
**需要确定两个参数**
存放数组首元素的指针变量
存放数组元素长度的整型变量
#include
int main()
{
double *p;
double x = 66.6;
p = &x;//x占8个字节 1个字节是八位,一个字节一个地址
//指针变量里存放首地址
double arr[3] = {1.1,2.2,3.3};
double *q;
q = &arr[0];
printf("%p\n",q); //%p实际就是以16进制输出
q = &arr[1];
printf("%p\n",q);
}
为什么会出现结构体:
为了表示一些复杂的数据,而普通的基本类型变量无法满足要求
什么叫结构体
结构体是用户根据自己需要自己定义的复合数据类型
如何使用结构体
注意事项
区别Java里的“类”与“结构体”
class Student
{
int sid;
String name;
int sage; //定义数据类型
void inputStudent()
{
}
void outputStudent()
{
} //具体操作
}
//java的“类”,包含定义的数据类型和操作
struct Student
{
int sid;
String name;
int sage; //定义数据类型
};
//结构体只包含一系列自定义的数据类型
例①:
#include
struct Student
{
int sid;
char name[200];
int sage;
}; //分号不要漏了
int main(void)
{
struct Student st = {100,"zhangsan",20};
printf("%d\n %s\n %d\n", st. sid,st. name, st. sage);
return 0;
}
#include
#include //用到了strcpy()函数,要在头文件定义;
struct Student
{
int sid;
char name[200];
int sage;
}st;
void student(struct Student *pst);
void g(struct Student st);
void g1(struct Student *pst);
int main(void)
{
//struct Student st; [为st分配好了内存,写在结构体结束的分号前也行]
student(&st);
g(st);
g1(&st); //g1是指针写法,记得把st的地址发送给形参
return 0;
}
//方法一:struct Student st = {100,"zhangsan",20}; =>st,sid [但是这种方法耗时间、耗内存,不推荐]
void g(struct Student st)
{
printf("%d\n %s\n %d\n", st. sid,st. name, st. sage);
}
//方法二:struct Student * pst = &st; =>*pst -> sid 指针pst指向结构体中 的sid这个成员
void g1(struct Student *pst)
{
printf("%d\n %s\n %d\n", st. sid,st. name, st. sage);
}
void student(struct Student *pst)
{
pst -> sid = 101; //写法二:(*pst).sid = 101;
strcpy( pst -> name,"lisi");//strcpy(参数1,"参数2");将参数2 copy到 参数2
pst -> sage = 21;
}
内存是CPU唯一可以直接访问的大容量存储区域,CPU只能访问内存,不能访问硬盘。
1、地址线:对哪个编号(地址)的单元进行操作 0-(4G-1):32位的电脑,2(32)-1=4G
地址:内存单元的编号
从0开始的非负整数,范围0-FFFFFFFF 0-(4G-1)
可以将地址看做一种数据类型,其int *或是String *
2、控制线:决定读还是写,只读或是只写
3、数据线:数据的双向传输。
**传统数组的缺点:**(传统数组即静态数组)
1、数组的长度必须事先制定,且只能是常整数,不能为变量
eg:
int a[5]; //ok
int len = 5; int a[len]; //error
2、传统形式定义的数组,该数组的内存,程序员无法手动释放。因此在一个函数运行期间,系统为该函数中的数组所分配的空间会一直存在。**直到函数运行完毕时,数组的空间才会被系统释放。**
3、数组的长度一旦被定义,其长度就不能再更改。
数组的长度不能在函数运行过程中动态地扩充或缩小。
4、A函数定义的数组,在A函数运行期间可以被其他函数使用,但在A函数运行完毕之后,A函数中的数组将无法被其他函数使用。
传统方式定义的数组不能跨函数使用。
**为什么要动态分配内存:**
1、动态分配内存很好地解决了传统数组的这四个缺陷
2、传统数组即静态数组
**动态数组的构造**
eg:
假设动态构造一个int型一维数组:
int *p = (int *)malloc(int len);
1、malloc是memory(内存)allocate(分配)的缩写
2、头文件添加# include
3、malloc函数只有一个形参,形参是整型
4、形参表示请求系统为程序分配的字节数
5、malloc函数只能返回第一个字节的地址 ,再通过强制类型转换,告诉分配的类型占几个字节,例如int 占4个,char占1个
6、p本身占4个字节,是静态的;p所指向的8个字节,是动态的
7、动态内存可以直接free(指针)掉 ,p本身的内存函数结束才自动释放
动态内存分配举例:
//动态地构造一维数组
int len;
printf("请输入需要构造的数组长度:");
scanf("%d",&len);
int * arr= (int *)malloc(4*len);
//对一维数组操作:赋值
printf("请输入数组的元素:");
for (i = 0;i<len;i++)
{
scanf("%d",&*(arr+i));
}
//输出
for (i = 0;i<len;i++)
{
printf("%d\n",*(arr+i));//也可以写成arr[i]
}
#include
int main(void)
{
int a[5] = {1,2,3,4,5};
int len;
int i;
printf("input the length:");
scanf("%d",&len);
int *ptest = (int *)malloc(sizeof(int) * len);//malloc函数只能返回第一个字节地址
//*ptest = 1;//类似于a[0] = 1;
//*ptest[1] = 2;//类似于a[1] = 2;
//我们可直接把ptest当做普通数组来用
for(i =0; i < len; i++)
scanf("%d",&ptest[i]);
for(i =0; i < len; i++)
printf( "%d\n", ptest[i]);
free(ptest);
return 0;
}
5、realloc(数组名,字节数):可以重定义已有的动态数组
静态内存和动态内存的比较
静态内存由系统自动分配,自动释放;
静态内存是在栈分配的
动态内存由程序员手动分配,手动释放
动态内存是在堆分配的
6、跨函数使用内存的问题
1、静态变量不可以跨函数使用内存
2、动态内存可以跨函数使用