今天主要是在边刷 OD 的机试真题边补算法和 C 语言语法。以题带点的学习相关的知识。
同时计划每天复习一下面试题,能做到流畅地输出出来。
C语言面试题
参考资料
1. 在1G物理内存的计算机中能否malloc(1.2G),为什么?
是有可能申请成功1.2G内存的。
malloc
申请的是堆区中的空间。
堆空间 只是操作系统划分出来的一大块 虚拟地址空间。
malloc
申请空间得到的是堆空间的地址,之后 程序运行 所需要的 实际物理内存是由操作系统映射完成的。
2. 什么是栈区、堆区和静态区
栈区
- 存放局部变量
- 由 CPU管理自动入栈和出栈,先进后出
- 连续的内存
- 每个线程有自己的栈区
堆区
- 动态存储
- 非常大的内存池,非连续分配
malloc
申请的内存就是从堆区分配的- 空间分配和释放由程序员自己管理
静态区
- 存储全局变量和静态变量
- 在程序的整个生命周期都存在。
3. sizeof
和 strlen
的区别是什么
sizeof
- 是一个 单目运算符,而不是函数
- 字节数的 运算在程序编译时进行,把sizeof的地方都替换成了具体的数字。
sizeof
返回变量或数据类型所占的内存字节数。- 如果是 字符串 则 会统计
'\0'
; 数组 则按照元素类型和数组长度来判断 整个数组占用的字节数。
strlen
- 是一个 函数,仅对字符串有效,遇到
'\0'
就结束,'\0'
不计入长度。
4. extern
的作用是什么
用来声明变量和函数作为外部变量或者函数供其他 .c
文件调用
5. malloc
、 calloc
和 realloc
有什么区别
malloc
的调用形式为 (类型*)malloc(size)
在内存的 堆区中分配一块长度为 size 字节的连续区域,返回该区域的首地址。
calloc
的调用形式为 (类型*)calloc(n, size)
在内存的 堆区中分配 n 块长度为 size 字节的连续区域,并且把分配的区域都初始化为 0,返回该区域的首地址。
realloc
的调用形式为 (类型*)realloc(ptr, size)
将ptr内存大小调整为 size 字节(扩大或缩小)。
6. 使用 malloc
申请空间和直接定义变量有什么区别
- 变量或数组的声明并没有分配内存,只是在使用的时候才分配内存。
例如
int arr[64000]
需要很大的内存,如果系统中没有这么多的字节,使用arr[index]
索引的时候就可能误操作其他的内存空间,导致无法预期的错误。
- 生命周期不同
局部变量 是 在栈空间中调用结束后就释放
而 全局变量 的生命周期是和程序的整个生命周期相同,无法自由释放。
这种情况就可以使用malloc
动态分配内存,可以自由申请和释放。 malloc
可以 申请一大块连续的内存 ,这一大块连续内存可以存储复杂的结构体和其他类型,还可以 灵活指定地址偏移,产生类似访问数组的效果。
7. malloc
的底层是怎么实现的
malloc ()
底层通过 brk ()
、mmap ()
等系统调用实现,根据申请内存大小不同 (小于 128k 在堆上分配,大于 128k 用 mmap ()
在文件映射区域分配)
8. static
有哪些用法
- 用static 修饰局部变量:使局部变量存储在 静态存储区,静态局部变量在函数执行完成之后不会被释放,而是继续保留在内存中。
- 用static 修饰全局变量:使其只在本文件内部有效,其他文件不可以链接或引用该变量。
- 用static 修饰函数:对函数的链接方式产生影响,使得 函数只在本文件内部有效,对其他文件是不可见的。这样的函数叫做静态函数,使用静态函数可以避免与其他文件的同名函数产生干扰,另外也是对函数本身的一种保护机制。
9. const
有哪些用法
- 用const 修饰变量:变量定义时就初始化,以后不能更改。
- 用const 修饰形参:该 形参在函数内部不能被修改。
- 用const 修饰函数:返回值是常量不能被修改。
被const修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。
// 这两个声明的作用是一样的,a是一个常整形数
const int a;
int const a;
// a 是一个指向常整型数的指针
// 不能通过指针去修改整型数
// 但指针本身可以被修改
const int *a;
// a 是一个指向整型数的常指针
// 也就是,可以通过指针去修改整型数
// 但指针本身不可以被修改
int * const a;
// a 是一个指向常整型数的常指针
// 也就是,不能通过指针去修改整型数
// 同时指针本身也不可以被修改))
const int * const a;
10. volatile
的作用和用法
volatile
用于 声明可能被意想不到地改变的变量,作用是 让编译器每次都重新读取其在内存中的值而非使用寄存器里的备份,常用于外围设备特殊功能寄存器、中断服务函数修改全局变量、多线程中共享变量等情况以避免因编译器优化而出现不符合预期的运行结果。
OD 机试题
2. 斗地主顺子
整体思路
step1. 读取一行输入的字符串,移除换行符。
step2. 分割输入的字符串为多个子串。
step3. 建立牌面映射表、定义比较函数对扑克牌进行排序。
step4. 存储可能的顺子顺序以及对应的长度。
step5. 生成顺子序列。
step6. 输出长度至少为 5 的顺子序列。
数据形式
step1 & 2.
假设用户输入 3 5 7 10 J <Enter>
则 input
= "3 5 7 10 J <Enter>"
分割并去除末尾回车后: cards
= {"3", "5", "7", "10", "J"}
扑克牌数量: count
= 5
step3 & 4.
排序后:cards
= {"3", "5", "7", "10", "J"}
动态分配顺子序列且初始化顺子序列及长度:
straights[0]
= {"3"}
straights[1]
= {"5"}
straights[2]
= {"7"}
straights[3]
= {"10"}
straights[4]
= {"J"}
step5 & 6.
假设cards
= {"3", "4", "5", "6", "7"}
初始化之后:
straights
= {{"3"}, {"4"}, {"5"}, {"6"}, {"7"}}
lengths
= {1, 1, 1, 1, 1}
开始生成顺子序列:
先从cards[0]
= "3"
开始,它无法插入任何顺子序列的末尾
再从cards[1]
= "4"
开始,它可以插入straights[0]
末尾,
从而straights
= {{"3", "4"}, {"4"}, {"5"}, {"6"}, {"7"}}
再从cards[2]
= "5"
开始,它可以插入straights[0]
straights[1]
末尾,
从而straights
= {{"3", "4", "5"}, {"4", "5"}, {"5"}, {"6"}, {"7"}}
再从cards[3]
= "6"
开始,它可以插入straights[0]
straights[1]
straights[2]
末尾,
从而straights
= {{"3", "4", "5", "6"}, {"4", "5", "6"}, {"5", "6"}, {"6"}, {"7"}}
再从cards[4]
= "6"
开始,它可以插入straights[0]
straights[1]
straights[2]
straights[3]
末尾,
从而straights
= {{"3", "4", "5", "6", "7"}, {"4", "5", "6", "7"}, {"5", "6", "7"}, {"6", "7"}, {"7"}}
最终lengths
= {5, 4, 3, 2, 1}
然后输出长度大于等于 5 的顺子序列,如果没有大于等于 5 的顺子序列,则输出 No.
语法解释
1. 动态内存分配 和 静态数组
下面的 straights
是一个二维指针数组,每个 straights[i]
都是一个二维指针
就像可以为一维指针 int *
分配内存空间,使得该指针可以当作一维 int
数组访问一样。
也可以对二维指针 int **
分配内存空间,使得该二维指针可以当作一维 int *
数组访问。
char **straights[count];
for (int i = 0; i < count; ++i) {
straights[i] = (char **)malloc(count * sizeof(char *));
straights[i][0] = cards[i];
如果把 straights
静态分配成一个二维 char *
数组,可以通过如下代码完成:
char *straights[count][count];
for(int i = 0; i < count; i++) {
straights[i][0] = cards[i];
}
2. qsort() 函数
qsort 的参数依次是:
待排序数组的起始地址
数组元素的个数
每个元素的大小(以字节为单位)
以及比较两个元素的函数
3. qsort()
回调的比较函数定义
比较函数的声明如下:
int compare(const void *a, const void *b);
参数传递 为 a 和 b 的地址,且 a 和 b 的值不可更改
返回值 为整数
返回0:两个数 a 和 b 相等
返回大于0:a 大于 b,则 qsort()
会在原数组中把 a 放在 b 的后面
返回小于0:a 小于 b,则 qsort()
会在原数组中把 a 放在 b 的前面
注意:比较函数中第一件事是把 a 和 b 进行类型转换。
由于是 传地址 ,所以传入的 a 和 b 实际为 &cards[i-1] 和 &cards[i],数据类型是 char **
需要解引用后得到实际的 cards[i-1] 和 cards[i]
const char *card1 = *(const char **)a;
const char *card2 = *(const char **)b;
4. strcspn()
函数
strcspn(input, "\n")
的意思是:计算字符串 input
中从起始位置开始,到第一个出现换行符 '\n'
之前的字符个数。
如果找到了换行符,就返回其索引;
如果没有找到,就返回整个字符串的长度。
5. strtok()
函数
strtok(input, " ")
的作用是将字符串 input
按照空格分隔成一个个子字符串(令牌),并返回第一个子字符串的指针,同时修改 input
(在分隔符处插入字符串结束符)以便后续调用继续获取后续的子字符串。
工作流程
假设有 input
= "3 4 5 6 7"
第一次调用 strtok(input, " ")
会查找第一个空格前的子串,并把空格变成 \0
,然后返回子串 "3"
所以 input
= "3\04 5 6 7"
第二次调用(注意 input 变成了 NULL) strtok(NULL, " ")
会接着刚才的 input 继续查找第一个空格前的子串,并把空格变成 \0
,然后返回子串 "4"
所以 input
= "3\04\05 6 7"
…
…
…
当所有的子串都被查找完后,strtok()
将返回 NULL
错误分析
- 映射表中 “2” 的值要比 “A” 大 2, 否则会成顺子
strcspn()
函数中,用双引号不是单引号。"\n"
- 在把当前的
cards[i]
添加到顺子序列末尾时,只有大 1 才添加,而不是不等就添加 - 把
cards[i]
添加到顺子序列末尾后,如果不break
,那么输出的是所有可以组成的顺子,而不是最大的顺子。
完整代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char card[3];
int value;
} CardMap;
CardMap cardMap[] = {
{"3", 3},
{"4", 4},
{"5", 5},
{"6", 6},
{"7", 7},
{"8", 8},
{"9", 9},
{"10", 10},
{"J", 11},
{"Q", 12},
{"K", 13},
{"A", 14},
{"2", 16}, // 3# "2"要比"A"大 2 ,否则 2 会认为是 A 的顺子
};
int mapLength = sizeof(cardMap) / sizeof(CardMap);
int compare (const void *a, const void *b) {
const char *card1 = *(const char **)a;
const char *card2 = *(const char **)b;
int value1, value2;
for(int i = 0; i < mapLength; i++) {
if (!strcmp(cardMap[i].card, card1)) {
value1 = cardMap[i].value;
}
if (!strcmp(cardMap[i].card, card2)) {
value2 = cardMap[i].value;
}
}
return value1 - value2;
};
int main() {
char input[100]; // 存放输入的大字符串
char *cards[20]; // 存放字符子串的数组
fgets(input, sizeof(input), stdin);
input[strcspn(input, "\n")] = 0;
int cardsNum = 0; // 记录字符子串的个数,即牌面个数
char *subString = strtok(input, " ");
while(subString != NULL) {
cards[cardsNum++] = subString;
subString = strtok(NULL, " ");
}
qsort(cards, cardsNum, sizeof(char *), compare);
char **straights[cardsNum]; // 顺子序列
int lengths[cardsNum]; // 顺子序列长度
for(int i = 0; i < cardsNum; i++) {
straights[i] = (char **)malloc(sizeof(char *) * cardsNum);
straights[i][0] = cards[i];
lengths[i] = 1;
}
int straightsNum = cardsNum;
for(int i = 0; i < cardsNum; i++) {
for(int j = 0; j < straightsNum; j++) {
// 1# compar() 是传地址,忘记 &
// 2# 不是当前牌和顺子序列末尾不等就添加,而是刚好大 1 才添加
if (compare(&cards[i], &straights[j][lengths[j] - 1]) == 1) {
straights[j][lengths[j]++] = cards[i];
break; // 4# 没有 break 的话,就会输出所有可能的顺子
}
}
}
int found = 0;
for(int i = 0; i < straightsNum; i++) {
if (lengths[i] >= 5) {
found ++;
for(int j = 0; j < lengths[i]; j++) {
if (j < lengths[i] - 1) {
printf("%s ", straights[i][j]);
}
else printf("%s", straights[i][j]);
}
printf("\n");
}
}
if (!found) {
printf("No\n");
}
return 0;
}
3. 数大雁
整体思路
step1. 获取输入的字符串
step2. 用贪心策略搜索字符串,搜查完一个完整的 "quack"
后,将其置空,然后接着搜索后面的所有 "quack"
并置空
step3. 重复上面的搜索,每次只要搜索到一个完整的 "quack"
就表示至少有一直大雁,5 只大雁就能把任意的字符串置空
step4. 如果找不到一个完整的 "quack"
则直接返回 -1
数据形式
step1.
假设输入 input
= "quqaucakck"
step2.
假设初始时 index = 0
quack = {'q', 'u', 'a', 'c', 'k'}
quackIndex = {-1, -1, -1, -1, -1}
i = 0
时,期望的目标 quack[index]
= 'q'
,当前的目标 quacks[i]
= 'q'
,匹配成功。
所以 index = 1
quackIndex = {0, -1, -1, -1, -1}
i = 1
时,期望的目标 quack[index]
= 'u'
,当前的目标 quacks[i]
= 'u'
,匹配成功。
所以 index = 2
quackIndex = {0, 1, -1, -1, -1}
i = 2
时,期望的目标 quack[index]
= 'a'
,当前的目标 quacks[i]
= 'q'
,匹配失败。
所以 index = 2
不变 quackIndex = {0, 1, -1, -1, -1}
不变
…
…
i = 7
时,期望的目标 quack[index]
= 'k'
,当前的目标 quacks[i]
= 'k'
,匹配成功。
所以 index = 5
quackIndex = {0, 1, 3, 5, 7}
此时搜索完一个完整的 quack
,根据 quackIndex
把原来字符串置空,然后接着搜索 quack
此时 input
= q u a ck
(接着从c位置开始搜)
i = 8
时,期望的目标 quack[index]
= 'q'
,当前的目标 quacks[i]
= 'c'
,匹配失败。
所以 index = 0
不变 quackIndex = {-1, 1, -1, -1, -1}
不变
step3.
上面的一个过程,把一只大雁发出的叫声全部搜索出来。
每次调用 find
函数,只负责找一只大雁发出的叫声, 如果能找到完整的 quack
,就说明至少有一只大雁
step4.
如果找不到完整的叫声,那么大雁的计数就为 0
完整代码
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#define MAXLENGTH 1000
bool find(char *quacks, int quacksLength) {
char quack[5] = {'q', 'u', 'a', 'c', 'k'}; // 贪心数组
int index = 0;
int quacksIndex[5] = {-1, -1, -1, -1, -1};
bool isFind = false;
for(int i = 0; i < quacksLength; i++) {
if (quacks[i] == quack[index]) {
quacksIndex[index++] = i;
}
if (index == 5) {
isFind = true;
index = 0;
for(int i = 0; i < 5; i++) {
quacks[quacksIndex[i]] = 0;
quacksIndex[i] = -1;
}
}
}
return isFind;
};
int main() {
char input[MAXLENGTH];
scanf("%s", input);
int quacksLength = strlen(input);
int least = 0;
while(find(input, quacksLength)) {
least ++;
}
printf("%d\n", least);
return 0;
}