序言
1. 内容介绍
本章详细介绍了数据结构基础内容、算法的相关功能,和复习C语言重点难点
2. 理论目标
- 掌握数据结构基础内容
- 掌握算法简介
- 复习C语言重点难点
3. 实践目标
- 无
4. 实践案例
无
5. 内容目录
- 1.数据结构基础内容
- 2.算法简介
-
- C语言重点难点复习
第1节 数据结构基础内容
编写程序的一般流程:
将问题抽象成一个数学模型;
问题涉及的数据量大小和数据之间的关系;
如何在计算机中存储数据及体现数据之间关系;
处理问题时需要对数据做何种运算;
编写程序的性能是否良好。
数据结构位于数学,计算机硬件、计算机软件三者之间的核心课程,是程序设计基础,而且是编译程序、操作系统及其它系统程序和大型应用程序的重要基础。
数据结构实例1:
通讯录中的姓名和电话,
记录之间是一对一线性关系或者
“一接一”的线性关系的数据结构。
数据结构实例2:磁盘目录结构,如下图所示,根目录下有很多子目录,子目录下又有多个子目录,是一种典型的
非线性结构-树形结构。一对多关系。
数据结构实例3:从一个地方到另外一个地方有多条路径。数据之间的关系是非线性结构-网状结构。多对多关系。
数据:是客观事物的符号表示。是指所有能输入到计算机中并被计算机程序处理的符号总称。
数据元素:数据的基本单位,在程序中作为一个整体来处理。
数据结构:是指相互之间具有一定联系的数据元素和对元素与元素之间能够进行的操作集合。
数据的逻辑结构
数据的存储结构
数据的逻辑结构,指的是元素之间的相互关系,与存储无关,是独立于计算机的,人脑中想象的。
数据的存储结构(物理结构),指的是逻辑结构在计算机中的存储实现,实实在在的,不是人脑想象的。
算法的设计取决于逻辑结构,算法的具体实现依赖于存储结构。
逻辑结构主要有两种:
线性结构:线性表、栈、队列等(一对一或者说“一接一”关系)。
非线性结构:树、图、多维数组、广义表等(一堆多,多对多关系)。
存储结构主要由两种:
顺序存储:地址空间连续的存储
链式存储:地址空间不连续的存储。
第2节 算法简介
什么是算法?
算法是对特定问题的求解步骤的一种描述,一个有穷规则(或语句、指令)的有序集合。对于问题的初始输入,通过算法有限步的运行,产生一个或多个输出。
算法描述实例:求两个正整数m,n的最大公因子。
① 输入m,n;
② m/n(整除),余数→r (0≤r≤n);
③ 若r=0,则当前n=结果,输出n,则算法停止;否则,转④;
④ n→m,r->n; 转②。
如初始输入 m=10,n=4,则m,n,r 在算法中的变化如下:
m n r
10 4 2
4 2 0(停止)
即10和4 的最大公因子为2。
算法研究的目的是为了更有效的处理数据,提高数据运算效率。数据的运算是定义在数据的逻辑结构上,但运算的具体实现要在存储结构上进行。一般有以下几种常用算法:
检索,指在数据结构中查找满足一定条件的数据。
插入,指往数据结构中增加新的数据。
删除,指从数据结构中删除指定的数据。
更新,指更新数据结构中的数据。
排序,指对数据结构中的数据进行排序。
算法的五大特性
有穷性:算法执行的步骤(或规则)是有限的;
确定性:每个计算步骤无二义性;
可行性:每个计算步骤能够在有限的时间内完成;
输入:算法有一个或多个外部输入;
输出:算法有一个或多个输出;
评价一个算法的好坏有以下几个标准:
正确性:算法应满足具体问题的需求。
可读性:算法容易供人阅读和交流。可读性好的算法有助于对算法的理解和修改。
健壮性:算法应具有容错处理。当输入非法或数据错误时,程序应该能识别并处理。
通用性:算法应该具有普适性,能对不同的数据类型集合都能处理。
效率和存储要求:在算法正确的情况下,算法应该遵循效率高,存储低的要求。
效率高指的是算法执行时间少;存储低指的是算法执行过程中需要的内存空间越少。
算法效率,通俗的讲,就是算法中每条语句执行时间之和,通常与问题的规模(参与运算的元素数量)有关。我们往往使用时间复杂度来衡量算法效率。
时间复杂度T(n) :将算法中的某个基本操作的执行次数的数量级作为算法时间复杂度的度量。这里讨论的时间复杂度不是执行完一段程序的总时间,而是其中基本操作的总次数的数量级。
算法中基本操作重复执行的次数是问题规模n (参与运算的元素数量)的某个函数f(n),T(n)=O(f(n)
它表示随问题规模n的增大,算法执行时间复杂度T的增长率和f(n)的增长率相同。
在算法题目中,通常问题的规模会用n表示,例如:要处理的数组元素的个数是n,而算法的基本操作的次数,通常是n的一个函数f(n)(这里的函数是数学中函数的概念)。对于求其基本操作执行的次数,就是求函数f(n)。
求出以后要取出f(n)中随n增大而增长最快的项,然后将其系数变为1作为时间复杂度的度量,记做T(n)= O(f(n)) 。
O(f(n)) 代表的是f(n)中增长最快的项,去掉前面系数后剩余的部分)。例如:f(n)=2n3+4n2+100,则其时间复杂度为T(n)=O(2n3)=O(n3)。
时间复杂度T(n)的量级通常有:
O©——常数级,不论问题规模多大,T(n)一致,因而是理想的T(n)量级;
O(n)——线性级;
O(n2),O(n3)——平方、立方级;
O(log2n),O(n*log2n) —— 对数、线性对数级;
O(2n)——指数级,时间复杂度T(n)最大,算法表现最差。
下图是几种常见的时间复杂度的比较
空间复杂度:指算法编写成程序后,在计算机中运行时所需存储空间大小的度量。记作:S(n)=O(f(n)); n为问题规模(参与计算元素的数量)。
第3节 C语言重点难点复习
引言:int a = 3;
int a;
向内存申请能够存放int类型数据的内存空间(4个字节),
用变量符号a代表,将来这块地址空间中要存放的内容。
a = 3;
将十进制数字3存入刚刚申请的内存空间。
print(“%d”,a)
将变量符号a表示的内存中存放的3打印出来。
思考:
能否不借助变量名称a,获取内存中存储的变量3。而是通过变量a或者说3所在的内存地址,获取内存中保存的内容3。
三个问题:
-
如何获取a或者说3所在的内存地址 ?
-
将这个内存地址保存在哪种类型的变量中 ?
-
如何通过内存地址获取内存中保存的内容 ?
再解决第三个问题:
- 如何通过p内存地址获取内存中保存的内容 ?
尝试:直接打印地址p
print(“%s”,p)“0x1A”显然只是内存的地址,不是其中的内容
解决方法:使用熟悉又陌生的符号 *
print(“%d”,*p) 3
p中的:解地址符 (解开地址,查看其中的内容)
读作: p解地址(对地址p进行解地址 )
p 读作:p解地址,这里的被称为解地址运算符(把地址解开,看看里面的数据)
(根据地址找到相应内存空间,看看空间里面存放的数据)
特征:*单独出现 (出现的场景:除了定义阶段)
int *p 读作:int为p所指
这里的*被称为 指针运算符(用来说明紧跟在后面的p是一个指针变量)
特征:类型和*同时出现 (出现的场景:定义阶段)
根据单独出现,还是与类型一起出现区分
*单独出现:“胆大,敢于解密(解地址运算符)”
*和类型一起出现:“和别人一起出现,胆子小,只能远远的指过去(指针运算符)”
有了指针后,访问内存中的存储单元,就有两种访问方式:
直接访问:通过变量名
例如: int a = 3;
printf(“%d”,a) 打印结果 3
间接访问:通过指针
指针变量是一种特殊的变量,变量p中存放的地址,这个地址是另一个变量a的起始地址。
指针变量的定义:
<数据类型> *<指针变量名>;
int *p;
int为p所指 (p指向一个int类型数据)
p=&a;p赋值为a取地址
p中存放的是a变量的地址。
变量指针:指的是某个变量的地址。
狭义上:“指针”与“地址”是同一概念。
广义上:指针是一个变量(地址类型的变量)
int *p;p是一个指针变量
p=0x1A;p这个指针变量可以赋值为某个具体地址0x1A
地址是一个常量 (地址类型的常量)
例如:0x1A是一个地址
总结:“指针”即“指针变量”即“地址类型的变量”
“变量指针”即“某个变量的指针”即“某个变量的具体地址”
例如:变量a的具体地址是0x1A
变量a的指针,即变量a的地址0x1A
注意:这里使用的“指针”是狭义的概念即“地址”
“变量指针”,“变量的指针”即“变量的地址”
概念总结:
“指针”,“指针变量”即“地址类型的变量”,例如 p,
“变量指针”,“某个变量的指针”即“某个变量的具体地址”,例如 0x1A。
在不影响理解的情况下,
“地址”、“指针”和“指针变量”可以不区分,通称“指针”。
指针p和变量a的关系
指针变量的定义和赋值:
<数据类型> *<指针变量名>;
比如: int *p;指针变量的定义,读作:int为p所指,
p=&a;指针变量的赋值,读作:p赋值为a取地址,
p中存放的是变量a的地址。
指针变量可以与其他变量一起定义,如:
int a, bb,*c;
char *p, *q, ch;
指针的引用(对指针进行解地址操作):
p 读作:p解地址,这里的被称为解地址运算符(把地址解开,看看里面的数据)
(根据地址找到相应内存空间,看看空间里面存放的数据)
特征:*单独出现
(出现的场景:除了定义阶段)
int a = 3; int *p; p=&a;
*p = 4 (相当于a=4)
*p ,p解地址,也就是对0x1A解地址,
解地址后实际是变量a,也就是数据3;再进行赋值操作,相当于a=4。
指针的引用(对指针进行解地址操作)的错误用法。
int *p; int类型数据为p所指
*p = 10;p解地址后赋值为10
在使用指针前,
必须给已定义指针变量赋值,然后才能使用。
如果p没有指向某个变量或某段内存空间,则不能给p所指向的变量赋值。
空指针,指的是地址为0的指针。
即0x00000000(16进制的0)
在<stdio.h>中,有宏定义,
将符号 NULL,定义为0号地址,即0x00000000
NULL直接翻译为英文,是“空”,
因此称NULL(0x00000000)为空地址。
注意:0x00000000即0号地址,系统保留不存入内容,专门为空指针指向使用。
空指针作用:在指针变量刚刚定义,还没有确定要指向哪个变量,往往被赋值为空指针,否则会指向随机位置(思考:为什么?)
int *p=NULL;//赋值为空指针
野指针:指针指向的对象已被销毁,则指针就变成野指针。
比如:int *p = &a;如果变量a所占的系统空间被释放,则系统将回收变量a原先占用的内存空间,系统很可能将这块内存另做他用,例如分配给变量b。
p就为野指针,此时应将p赋值为空指针。
p=NULL
指针运算是以指针变量所存放的地址作为数值而进行的运算。因此,指针运算的实质就是地址的计算。
指针运算的种类是有限的,它只能进行算术运算、关系运算和赋值运算。
指针运算中的关系运算
回顾关系运算符
,>, >=,!=,<,<=,==
如果有指针,
显然p<q,地址靠后的即数值较大的指针,更大。
注意:
指针比较大小,不是指针所指向的变量进行比较大小。
指针比较大小,不是指针所指向的变量进行比较大小。
显然p<q(地址靠后的即数值较大的指针,更大。)
指针运算中的算术运算
回顾算术运算符
,+, -,++,–
思考:如果对指针进行+1操作,从0x1A变为0x1B是否有意义呢?
指针运算中的算术运算
思考:p+1的是实质是什么(其中p的定义:int *p)
Øp+1表示的实际位置的地址量是:
p + sizeof (p指向的数据类型占内存大小) * 1
p-n表示的实际位置的地址量是:
§ - sizeof (p指向的数据类型占内存大小) * n
两个指针减法运算
前提:指针类型必须相同,相减结果是两指针之间相隔数据的个数。
q-p 运算的结果是两指针指向的地址位置之间相隔数据的个数是2
计算过程 : q-p = (q地址数值-p地址数值)/sizeof(数据类型)
=( 0x19-0x11)/(4) = 8/4 = 2
两个指针减法运算
前提:指针类型必须相同,相减结果是两指针之间相隔数据的个数。
q-p 运算的结果是两指针指向的地址位置之间相隔数据的个数是2
计算过程 : q-p = (q地址数值-p地址数值)/sizeof(数据类型)
=( 0x19-0x11)/(4) = 8/4 = 2
复习指针的引用(对指针进行解地址操作):
p 读作:p解地址,这里的被称为解地址运算符(把地址解开,看看里面的数据)
(根据地址找到相应内存空间,看看空间里面存放的数据)
特征:*单独出现
(出现的场景:除了定义阶段)
int b = 3; int *p; p=&b;
*p = 4 (相当于b=4)
*p ,p解地址,也就是对0x1A解地址,
解地址后实际是变量b,也就是数据3。
再进行赋值操作,相当于b=4。
数组:int a[5] = {1,2,3,4,5};
思考:打印printf(“%d”,a)得到什么结果?
数组名表示的是数组首元素的起始地址
为了便于理解,可以当做有一个和数组名称相同的变量a,存放的是数组首元素地址
数组:int a[5] = {1,2,3,4,5};
思考:根据之前数组的学习,a[1]显然是数组元素2,
既然a作为数组名实际是首元素地址,
那么a[i]底层有怎样的运算呢?
分两步:
-
从首元素第0个元素的地址a,计算出第1个元素的地址。
-
对第1个元素的地址进行解地址操作,求出其中的数据
1.从首元素第0个元素的地址a,计算出第1个元素的地址。
计算过程:a+1 = a + sizeof (int) 1 = 0x1A + 41 = 0x1E
2.对第一个元素的地址进行解地址操作,求出其中的数据
计算过程:(a+1)=(0x1E)=2 (其中的*是解地址运算符,不是指针运算符)
总结:
a 表示的是a[0]元素的地址。
a[i] 的实质是*(a+i)。
由于数组名a是指针常量,因此为更方便操作数组元素,通常将数组地址赋值给其他指针变量
例如p,然后通过指针来操作。
通过指针遍历一维数组
注意:
指针变量p和数组名a使用方法具有相同形式,但两者本质上是不同的
指针变量是地址变量
数组名是指针常量
例如:
指针变量:p++; (p=p+1) p指向下一个元素
数组名:a++; (a=a+1) 不可以,a是指针常量,不能被赋值。
总结:int *p;
• int a[100];
• p = a;
a[i] 、(a+i) 、(p+i) 和p[i]完全等价:访问数组第i个数组元素。
&a[i] 、a+i、p+i 完全等价:都是第i个元素的地址。
取地址运算符(&):与解地址运算符(*) 功能相反的运算符
&p 读作:p取地址
使用方法:&放在已经定义的变量名称前面
功能:获取某个变量的地址
例如:&a[1](读作a[1]取地址) 打印&a[1] 将得到0x1E 和 a+1相同
同理:打印&a[0] 将得到0x1A 和 a 相同
总结:
访问数组a中第 i 个数组元素: a[i] ,*(a+i)
获取数组a中第 i 个元素的地址: &a[i] ,a+i
复习:数组指针和指针数组:从定义方式的阅读中区分(* 读作:为……所指)
1.数组指针(指向数组的指针)
int (*p)[4]
读作: int类型 数组[4] 为p所指
在这里直接将[ ],读作数组,因为[ ]为数组的标志,同时这样阅读,也能明确指针指向的是数组并且明确了数组中元素数量是4个。
2.指针数组(装着指针类型数据的数组,就是指针数组)
int *p[4]
读作:int类型为p[4]所指
p[4]显然是指p[4]元素,而这个元素所指向的是int类型的数据,显然这是一个装着指针的数组,是指针数组(这里的p[4]元素是为了便于理解,数组元素只有p[0]到p[3])
字符数组名称p是二级指针
int a = 3; 读作: 初始化int类型变量a,a赋值为3
int *p=&a; 读作: int为p所指,p赋值为a取地址
int **q = &p; 读作:int 为q所指的所指,q赋值为p取地址
int **q ;
int 为q所指的所指
解读:q所指向的是p(p是一个指针),p所指向的是int类型的数据
** 读作:为…所指的所指(不建议读作”星星”)
p为一级指针变量(数值是0x1A), *p是指对0x1A解地址,得到变量a,值是3。
q为二级指针变量(数值是0x3A), *q是指对0x3A解地址,得到变量p,值是0x1A。
q是一个指针变量(里面存放地址),q中存放的地址是p,而p也是一个指针变量,该地址存放这一个整型数据3。
二级指针是指向指针数据的指针变量,简称为指向指针的指针。