目录
函数注意递归结束条件并需要在递归前先判断。
注意全局变量的数据段空间与堆空间不冲突,另外变量采用就近原则。
结构体的对齐原则以数据存放的偏移地址为对齐标准,最后会在末尾补全为最大基类型的整数倍。
结构体指针同样表示结构体的起始地址。
C++的引用其作用是给变量的地址起一个别名。
逻辑结构与存储结构的对象不同。
时间复杂度:算法中所有语句的频度(执行次数)之和。
顺序表的定义增删改查逻辑。
链表的定义增删改查逻辑。
理解结构体指针对于链表的定义更深刻。
OJ练习
申请动态内存
输入一个整型数,然后申请对应大小空间内存,然后读取一个字符串(测试用例的字符串中含有空格),字符串的输入长度小于最初输入的整型数大小,最后输出输入的字符串即可(无需考虑输入的字符串过长,超过了内存大小)
OJ不支持fflush(stdin)清空标准输入缓冲区操作,因此在fgets之前先利用scanf清空缓存区中遗留的\n。另外,C11标准去掉了gets,部分学校机试不可以使用gets,建议使用fgets。
#include <stdio.h>
#include <stdlib.h>
int main() {
int n;
char *p;
scanf("%d", &n);
p = (char *) malloc(n);
char c = scanf("%c", &c);
fgets(p, n, stdin);
puts(p);
return 0;
}
函数的声明与定义
函数之间的调用关系是:由主函数调用其它函数,其它函数之间可以互相调用。同一个函数可以被一个或多个函数调用任意次,同样也可以调用任意次其它函数。
可以嵌套调用,但不支持嵌套定义。
递归调用
递归就是方法自己调用自己,递归函数要注意先判断结束条件,否则会产生死循环。
递归的核心是找公式。
n阶乘:f(n)=n*f(n-1),if(1==n) return 1;
n台阶:step(n)=step(n-1)+setp(n-2),if(1==n||2==n) return n;
全局变量与局部变量
全局变量存放在数据段空间中,在程序的全部执行过程中都占用存储空间,而不是仅在使用时才开辟单元,只有程序结束才会被释放。
第一个print(i)获取数据段空间中的全局变量i,i=5同样也是全局变量i进行了值的修改,第二个prnt(i)仍未全局变量i,int i=6是在栈空间中的变量,第三个print(i)的仍未全局变量i=5。
如果局部变量与全局变量重名,采用就近原则,即实际获取和修改的值是局部变量的值。
形参可以看成一个局部变量的就近原则。
局部变量只在离自己最近的结构体中有效。
关于实参与形参的说明:
- 若函数没被调用,那么该函数中的形参不占用内存中的存储单元,只有该函数被调用时该函数中的形参才会分配内存。
- 实参可以是常量、变量或表达式,要求它们有确定的值。
- 定义函数时,必须指定形参的类型,同时实参应遵守个数和类型与形参的一一对应关系。
- 实参向形参的数据传递时单向“值传递”,只能由实参传递给形参。函数调用结束后,形参单元被释放,实参单元仍保留并维持原值。
- 形参相当于局部变量,因此不能再定义局部变量与形参同名。
- 实参是指传递给函数的值。
结构体
将不同类型的数据组合成一个整体就需要通过结构体进行管理。
声明的一般形式:
struct 结构体名
{
成员列表(基类型 类型名;...)
};
struct Student
{
int num; char name[20]; char sex;
int age; float score; char addr[30];
}
先声明结构体类型,再定义变量名。
struct student student1, student2;
结构体对齐
结构体的大小遵循对齐原则
- 默认结构体起始地址为0。
- 各成员变量对应的起始偏移量能被各自大小整除对齐。
- 结构体整体大小是其成员变量的最大基类型的整数倍。
- 嵌套结构体的对齐量为被嵌套结构体的最大基类型。
例如上述结构体Studen的大小为68B。
C=1+3+(3+1+4)+1+3=16
结构体指针
结构体指针就是该变量所占据的内存段的起始地址。
成员访问方式:
- (*p).name。由于.的优先级高于*,因此需要用括号(*p)。
- p->name。指针对象访问成员。
- p=sarr;等价于p=&sarr[0];
- 指针偏移在数组中的使用方法仍适用。
- malloc申请空间:p=(struct Student*)malloc(sizrof(struct Student));
typedef
起别名,在结构体中应用广泛。
stu等价于struct Student,pstu等价于struct Student*.
typedef struct Student {
int num;
char name[20];
char sex;
}stu, *pstu;
int main() {
stu s = {0};
struct Student s1 = {0};
stu *p=&s;
pstu p1 = &s1;
return 0;
}
C++引用和布尔型
当在子函数中要修改主函数中变量的值,就需要用到引用。
其作用是给变量的地址起一个别名。
modify_num0为纯C函数,modify_num为C++函数。
modify_pointer0为纯C函数,modify_pointer为C++函数。二级指针到指针引用的使用。
C++扩充了布尔类型,true和false,即1和0。
OJ练习
引用指针到申请空间
使用C++的引用,注意提交时把代码选为C++;在主函数定义字符指针 char *p,然后在子函数内malloc申请空间(大小为100个字节),通过fgets读取字符串,然后在主函数中进行输出;要求子函数使用C++的引用。
注意在C++中从标准输入读取字符串,需要使用fgets(p,100,stdin),最后free释放申请的空间。
#include <stdio.h>
#include <stdlib.h>
void malloc_char(char *&p)
{
p=(char *)malloc(100);
fgets(p, 100, stdin);
}
int main()
{
char *p;
malloc_char(p);
puts(p);
free(p);
return 0;
}
逻辑结构与存储结构
逻辑结构:
- 集合结构——无关系
- 线性结构——一对一
- 树形结构——一对多
- 图形结构——多对多
存储结构:
- 顺序存储:逻辑上相邻的数据元素,其物理存放地址也相邻。优点是存储密度大,存储空间利用率高;缺点是插入或删除元素时不方便。
- 链式存储:逻辑上相邻数据元素物理地址可以随意存放,但所占存储空间分两部分,一部分存放结点值,另一部分存放表示结点间关系的指针。优点是插入或删除元素时很方便,使用灵活。缺点是存储密度小,存储空间利用率低。
- 索引存储
- 散列存储
顺序存储于链式存储分析:
-
顺序表适宜于做查找这样的静态操作。
-
链表宜于做插入、删除这样的动态操作。
-
若线性表的长度变化不大,且其主要操作是查找,则采用顺序表;
-
若线性表的长度变化较大,且其主要操作是插入、删除操作,则采用链表。
时间复杂度与空间复杂度
算法的定义:对特定问题求解步骤的描述。
特性:有穷、确定、可行、输入、输出。
时间复杂度:算法中所有语句的频度(执行次数)之和。记作:T(n)=O(f(n))。
n是问题的规模,f(n)是问题规模n的某个函数。表示随问题规模n的变化,算法执行时间的增长率和f(n)的增长率相同。
只选取最高阶次项,最高阶越小,算法的时间性能越好。
2 4 8 16
时间复杂度计算忽略高阶项系数和低阶项。
空间复杂度:算法过程中所使用的辅助空间(随着n的增长而增长的空间)的大小。记作:S(n)=O(f(n))。
线性表
由n(n>=0)个相同类型的元素组成的有序集合。
线性表中元素个数n,称为线性表的长度。当n=0时,为空表。
ai-1为ai的直接前驱,ai+1为ai的直接后驱。
逻辑结构特点:
- 元素个数有限。
- 元素数据类型都相同。
- 具有逻辑上的顺序性,在序列中个元素排序有先后顺序。
顺序表
线性表的顺序表示简称顺序表。
逻辑上相邻的两个元素再物理位置上也相邻。
len是顺序表中实际存放的元素个数,即顺序表的当前长度。
顺序表初始化及其插删查代码实现
#include <stdio.h>
#define MaxSize 50
typedef int ElemType; //让顺序表存储其它类型元素时,可以统一快速修改
typedef struct {
ElemType data[MaxSize];
int length;//顺序表长度
} SqList;
//打印顺序表
void PrintList(SqList L) {
for (int i = 0; i < L.length; i++) {
printf("%3d", L.data[i]);
}
printf("\n");
}
顺序表的插入
//顺序表的插入,由于表L会改变,因此这里用引用,i是插入位置,lement是插入的元素
bool ListInsert(SqList &L, int i, ElemType element) {
//判断插入位置i是否合法,存储空间是否已经占满
if (i < 1 || i > L.length + 1 || MaxSize == L.length) {
return false;//插入失败返回false
}
//把后面的元素依次往后移动,空出位置,用来放入要插入的元素
for (int j = L.length; j >= i; j--) {
L.data[j] = L.data[j - 1];
}
L.data[i - 1] = element;//放入要插入的元素
L.length++;//成功插入后顺序表长度+1
return true;//插入成功返回true
}
顺序表的删除
//删除顺序表中的元素,表L会改变,i是删除元素的位置,e是为了获取被删除元素的值
bool ListDelete(SqList &L, int i, ElemType &e) {
if (i < 1 || i > L.length) {
return false;
}
e = L.data[i - 1];//保存要删除的元素值
//往前移动元素
for (int j = i; j < L.length; j++) {
L.data[j - 1] = L.data[j];
}
L.length--;//成功删除后顺序表长度-1
return true;
}
//按值删除,注意不要跳逻辑写代码,容易出现细节问题导致程序错误
bool DeleteList(SqList &L, ElemType element) {
int j = 0, remLen = 0;
for (int i = 0; i < L.length;
i++) {
while (element == L.data[j] && j < L.length) {
j++;
remLen++;
}
L.data[i] = L.data[j++];
}
L.length -= remLen;
if (remLen)return true;
else return false;
}
顺序表的查找
//查找某个元素的位置,找到了返回对应位置,没找到返回0
int LocateElem(SqList L,ElemType element){
for (int i = 0; i < L.length; ++i) {
if(element==L.data[i]){
return i+1;//因为i是数组的下标,+1表示顺序表的位置
}
}
return 0;//遍历完没找到返回0
}
主函数:
int main() {
SqList L;//定义顺序表L
bool ret;//ret用来存放操作函数的返回值
L.data[0] = 1;//放置元素
L.data[1] = 2;
L.data[2] = 3;
L.length = 3;//设置长度
//顺序表的插入
ret = ListInsert(L, 2, 7);
if (ret) {
printf("insert into sqlist successful \n");
PrintList(L);
} else {
printf("insert into sqlist failed\n");
}
//顺序表的删除
ElemType del;
ret = ListDelete(L, 2, del);
if (ret) {
printf("delete %d in sqlist\n", del);
PrintList(L);
} else {
printf("delete sqlist failed\n");
}
//顺序表的查找
int pos;
pos = LocateElem(L, 3);
if (pos) {
printf("find this element in %d\n",pos);
} else {
printf("don't find this element\n");
}
return 0;
}
单链表
线性表的链式表示简称链表。
头指针是链表的必须元素,标识该链表。不论链表是否为空,头指针永不为空。存在头结点时,头指针永远指向头结点。
头结点的数据域为空或链表长度。定义头结点后对于在第一结点前插入和删除第一节点的操作就统一了,不需要频繁重置头指针。但头结点不是必须的。