历年笔试题目笔记

#应试笔记与八股

目录

Readme

要点:

  1. 复试时记得带表
  2. 别忘了释放内存(尤其是自创链表的操作题)
  3. 冒泡、快排一定要会背
  4. 以下函数的参数列表
    1. fgetc
    2. fgets
    3. getsgets(char* s)
    4. memcpymemmove
    5. qsortvoid qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void *))
  5. 翻转链表一定要背,2019年计算机学院的链表区域截断一定要多练。记得考虑数组、类数组法解题。
  6. 必须会背P267的内存题,该题出现在2023计院、2018计院、2017计院、2015计院多次出现。

必背代码

1. 冒泡排序

基本原理
从前向后(或从后向前)两两对比并按照目标顺序交换位置。每一趟均可确定未排序序列中最值元素的最终位置并使得下一趟排序中少进行一次比较。对于 n 个元素的序列需要执行 n-1 趟,对比次。

void bubble_sort(int nums[], int n) {
    // 排序共计循环 n-1 趟
    for(int trips = 0; trips < n - 1; trips++) {
        // 对前n-trips个元素进行冒泡,比较nums[i]和nums[i+1]的大小,i \in [0, n - trips - 2]
        for(int i = 0; i < n - trips - 1; i++) {
            if(nums[i] > nums[i + 1])
            {
                int temp = nums[i];
                nums[i] = nums[i + 1];
                nums[i + 1] = temp;
            }
        }
    }
}

2. 快速排序

基本原理
使用分治的基本思想,选择一个元素作为枢轴元素(pivot),通常选择第一个或者最后一个元素,并通过元素交换使得所有左侧元素小于等于枢轴、右侧元素大于等于枢轴;并对枢轴左右两侧序列递归进行快速排序。

具体细节:

  1. 在序列头部(或尾部)选择一个枢轴元素并提出,使原位为空
  2. 随后在序列头部和尾部分别布设一个指针,当左侧指针为空位且右侧指针所指元素比枢轴元素大,则和左侧空位交换;
  3. 直到左右指针汇合,最终位置即为枢轴元素位置。
  4. 随后对枢轴元素左右子串进行递归快排处理。
// 选择枢轴元素、处理数组并确定枢轴最终位置
int partition(int nums[], int low, int high)
{
	int pivot = nums[low];
	while(low < high)
	{
		while(low < high && nums[high] >= pivot) high --;
		nums[low] = nums[high];
		while(low < high && nums[low] <= pivot) low ++;
		nums[high] = nums[low];
	}
	// 此时 low = high ,且为pivot的最终index
	nums[low] = pivot;
	return low;
}

void quick_sort(int nums[], int low, int high)
{
	if(low < high)
	{
		// 选择枢轴元素并处理数组,返回枢轴元素最终位置
		int pivot = partition(nums, low, high);
		// 对枢轴左右未排序序列递归进行快排
		quick_sort(nums, low, pivot - 1);
		quick_sort(nums, pivot + 1, high);
	}
}

3. 二分查找

int binary_search(int nums[], int n, int target) {
    int l_index = 0, r_index = n - 1;
    while(l_index <= r_index)
    {
        int mid = (l_index + r_index) / 2;
        if(nums[mid] == target)
            return mid;
        else if(nums[mid] > target)
            r_index = mid - 1;
        else
            l_index = mid + 1;
    }
    return -1;
}

4. 链表递归释放

// 递归free链表
void destory_list(node_t* list)
{
    if(list != NULL)
    {
        destory_list(list -> next);
        free(list);
    }
}

5. 辗转相除法求最大公约数

辗转相除法是用于求两个非负整数 ab 的最大公约数(gcd、Greatest Common Divisor)的方法,其基本逻辑如下:
直到
时,就可得:

注意:

  1. 要求 a 大于等于 b
// a must geq than b.
int gcd(int a, int b)
{
    int mod = a % b;
    if(mod)
        return gcd(b, mod);
    else
        return b;
}

6. 最小公倍数

用二者之中小值乘最大公约数即可。

7. 链表原地倒置(不含头结点)

递归不是很好处理,直接双指针。

typedef struct node
{
    int id;
    struct node* next;
} node_t;

node_t* reverseList(node_t* list) {
    // 窗口中存储上一节点和当前节点
    node_t* prev = NULL;
    node_t* curr = list;
    while(curr)
    {
        // 缓存下一个要处理的节点
        node_t *next = curr -> next;
        // 将当前节点拆出,使其等于上一个节点
        curr -> next = prev;
        // 后移窗口
        prev = curr;
        curr = next;
    }
    return prev;
}
  1. Eratasthene筛选法求素数(TODO)

会写代码

1. 直接插入排序

基本原理:
每次将一个待排序的记录按关键字大小插入到前面已经排好序的序列中,直到全部记录插入完毕。

即当从左向右、从小到大排序时,已排序序列位于左侧,然后依次将右侧未排序序列中第一个元素插入到左侧已排序序列中。

void insert_sort(int array[], int n)
{
	for(int i = 1; i < n; i++)
	{
		for(int j = i - 1; j >= 0; j--)
		{
			if(array[j] > array[i])
			{
				swap(&array[i], &array[i]);
			} else {
				break;
			}
		}
	}
}

必背知识点列表

  1. 运算符优先级:

    2.8 讲一讲运算符的运算顺序

    几个比较重要(常考)的运算优先级

    1. . 运算符取成员的优先级大于 * 解引用的优先级,例如:
      • *p.member 错误
      • (*p).member 正确
    2. int (*p)[3]p 为一个指向有3个 int 型变量的数组的指针
    3. int *p[3]p 是一个有3个 int* 变量的数组
    4. a = *p++ ,先对 p 解引用,随后 p 自增
    5. a = *(p++) ,同上
    6. a = (*p)++ ,对 p 解引用,并对 p 指向的对象自增

    C Programming: A Modern Approch, Second Edition (K.N.King) 的附录A中给出了如下优先级表:

    优先级
    名称
    符号
    结合性
    1 数组取下标 [] 左结合性
    1 函数调用 () 左结合性
    1 取struct和union成员 .-> 左结合性
    1 自增(后缀) i++ 左结合性
    1 自减(后缀) i-- 左结合性
    2 自增(前缀) ++i 右结合性
    2 自减(前缀) --i 右结合性
    2 取地址 & 右结合性
    2 间接寻址 * 右结合性
    2 一元正号 + 右结合性
    2 一元负号 - 右结合性
    2 按位求反 ~ 右结合性
    2 逻辑非 ! 右结合性
    2 计算变量空间 sizeof 右结合性
    3 强制类型转换 (type) 右结合性
    4 乘法类运算符 */% 左结合性
    5 加法类运算符 +- 左结合性
    6 移位 >><< 左结合性
    7 数学关系符(除判等) ><>=<= 左结合性
    8 判等关系符 ==!= 左结合性
    9 按位与运算 & 左结合性
    10 按位异或 ^ 左结合性
    11 按位或 | 左结合性
    12 逻辑与 && 左结合性
    13 逻辑或 || 左结合性
    14 三目条件运算符 ? : 右结合性
    15 赋值运算符 =*=/= ... 右结合性
    16 逗号 , 左结合性

    此外,CPP Reference也给出了C语言运算符优先级表,算是比较权威的资料,与上述资料基本印证(区别在于上述资料把部分同级但结合性不同的运算符拆分为两个优先级了)

    同样的,CPP Reference也给出了求值顺序中的部分未定义行为
    例如经典的未定义行为:

    1
    int i = 3;
    2
    int k = (++i)+(++i)+(++i); //15? 16? UB?

    gcc 9.2.0 下结果是 16
    而在 clang++ 17.0.6 下是 15
    在上述规则表中,+ 优先级低于 ++i ,但依旧无法处理。

    对于普通表达式,可以按照上述方式进行运算:

    1
    int a = 7 + 8 * 5 - 6;

    式中有运算:

    • 二元加减法,优先级4
    • 二元乘除法,优先级3
      则应当先计算乘法,有:
    1
    int a = 7 + 8 * 5 - 6;
    2
    = 7 + 40 - 6

    随后看同级运算中的结合顺序,加减法的结合顺序为从左至右,即:

    1
    int a = 7 + 8 * 5 - 6;
    2
    = 7 + 40 - 6;

    尽管是未定义行为,但是应试不得不计算时可按照如下方式尝试计算:

    1
    int i = 3;
    2
    int k = (i++)+(i++)+(i++);
    3
    = (3)+(4)+(5);
    4
    = 12;

    常考的优先级顺序有:
    1. 取对象成员的 ->. 运算符优先级低于解引用运算符 * ,故必须 (*p).num
    2. 后置的自增运算符 p++ 优先级高于解引用运算符 * ,故 a = *p++ 等价于 a = *(p++) ,又由于延迟自增,故该表达式等价于 a = *p; p++;

改错题常见错误列表

  1. 边界检查
  2. 变量类型错误
  3. 指针赋值时是否取地址
  4. 字符串末尾 \0 问题

2023年计算机学院笔试题目(Finish)

1. 简答题(40分)

1.1 简述一下C语言采取了哪些措施提高运行效率(10分)

  1. 允许程序直接访问内存,免去了各种抽象接口所造成的的额外开销。
  2. 使用指针特性,允许直接操作内存地址,可以直接访问内存中的数据,而无需进行额外的拷贝操作。
  3. 使用内联函数,允许编译器将部分函数代码直接插入到目标函数中,减少了函数调用的开销。
  4. 允许编译器对代码进行优化,使用重排、循环展开、函数内联等方式提高了生成的机器代码的执行效率。
  5. 提供低级语言特性,C语言提供了例如位操作、嵌入汇编等方式直接操作硬件,减少了代码的层次,提高了效率。
  6. 精简了标准库,减少了生成的机器代码的大小,提高了执行效率。
  7. 提供了宏功能(Macro),允许用户使用宏函数等方法减少了函数调用,将部分运算(例如sizeof)直接在预处理期完成。

1.2 内存计算题(10分)(重要、多做)

做P267的2015年第4题。

1.3 函数结构与显示语言表示(10分)

下列代码:

for(int i = 0; nums[i] != temp; i++)
{
    printf("%d", i);
}

是什么程序结构?使用显示结构语言该如何表示?并标出条件跳转和强制跳转。

答案:
在黄迪明的《C语言程序设计》一书中,程序被划分为三种基本结构:

  1. 顺序结构
  2. 分支结构
  3. 循环结构

因此该程序的结构为循环结构,当 nums[i] != temp 时执行循环,当 nums[i] == temp 时结束循环。

其流程图为:
Pasted image 20240316201601.png

显示表示为:

int i = 0;
L1:
if(nums[i] != temp)
    goto L2;        // 条件跳转
else
    goto L3;        // 条件跳转
L2:

printf("%d", i);

goto L1;            // 强制跳转

L3:

1.4 在array中搜索item(10分)

假设 arr 为整形数组, numitem 为整形变量, 。需要查找 item 是否在数组中,如果程序片段为:

for(num = N; arr[num] != item; num--)
	printf("%d", num);

可能会引发什么异常?

答:

  1. 该整形变量 num 开始递减,直到 arr[num] == item
  2. 其可能触发数组越界访问异常,因为若 arr 中不存在 item ,则 num 会不停递减,直到递减到 num = -1 时进行 arr[num] != item 表达式判断时触发异常。

2. 编程题(160分)

2.1 字符串编程(40分)

给出两个字符串,判断第一个字符串中有几个内容等于第二个字符串的子串。
例如给定字符串A ababaaabbb ,字符串B ab

  1. 窗口内存对比法(使用 memcmp 函数)
int calculate_substr(char *A, char *B)
{
    int len_A = strlen(A);   //计算字符串A、B的长度
    int len_B = strlen(B);
    int substr_num = 0;      //A的子串数量
    if(lenA < len_B)
        return substr_num;
    for(int i = 0; i < len_A - len_B + 1; i++)
    {
        if(!memcmp(&A[i], B, len_B)) //指针也可以使用字符串引索
            substr_num++;
    }
    return substr_num;
}
  1. 使用子字符串搜索(使用 strstr 函数),如果需要简化边界条件则会增加搜索次数(如下)
int calculate_substr(char *A, char *B)
{
	int *start_pos = A;       //字符串A的起始搜索地址
    int len_B = strlen(B);    //计算字符串B的长度
    int substr_num = 0;       //A的子串数量
	while(start_pos = strstr(start_pos, B))
	{
		substr_num ++;
		start_pos ++;         //实际上可以加2,但是会使边界条件复杂化
	}
    return substr_num;
}

2.2 文件&字符串编程(40分)

给出一个英语文件作为输入,统计有多少不同单次还有每个单次的数量,要求使用结构体数组完成,可以用 strcmp 函数判断字符串是否已经存在。

数组法:

#define MAX_WORD_LEN  32
#define MAX_WORD_NUM  1024

#include <string.h>
#include <stdio.h>

typedef struct {
    char word[MAX_WORD_LEN];
    int show_times;
} word_t;

word_t library[MAX_WORD_NUM] = { 0 };

int search_word(char word[])
{
    for(int i = 0; i < MAX_WORD_NUM; i++)
    {
        if(library[i].show_times == 0)
        {
            return -1;
        } else if(!strcmp(library[i].word, word))
        {
            return i;
        }
    }
}

int count_word(char word[])
{
    int word_id = search_word(word);
    if(word_id > 0)
    {
        library[word_id].show_times++;
        return word_id;
    } else {
        for(int i = 0; i < MAX_WORD_NUM; i++)
        {
            if(library[i].show_times == 0)
            {
                strcpy(library[i], word);
                library[i].show_times = 1;
                return i;
            }
        }
        return -1
    }
}

int process_file(char path[])
{
    FILE *f = fopen(path, "r");
    if(f == NULL)
    {
        printf("open file: %s failed.\r\n", path);
        return -1;
    } else {
        char word[MAX_WORD_LEN] = { 0 };
        while(fgets(word, MAX_WORD_LEN, f))  //fgets函数当遇到换行符时会自动分割并返回word指针,遇到EOF时会返回NULL。
        {
            count_word(word);
            memset(word, 0x00, MAX_WORD_LEN);
        }
        return 0;
    }
}

// C语言的命令行参数中,第一个参数通常为可执行文件的路径,第二个参数开始才是命令行传入的参数
int main(int argc, char argv[])
{
    // load English file.
    if(!process_file(argv[1]))
        return -1;
    
    // A demostration of search a word "hello"
    char test_word[] = "hello";
    int index = search_word(test_word);
    if(index >= 0)
        printf("The show times of word %s is %d\r\n", test_word, library[index]);
    else
        printf("The show times of word %s is 0", test_word);
    return 0;
}

2.3 冒泡排序和折半查找(40分)

结构体中存有学生姓名、学号、分数三个信息。要求:

  1. 用冒泡排序将成绩从低到高排序
  2. 用折半查找找到分数为 87 的所有学生的信息
#include <stdio.h>

typedef struct{
    char name[16];
    int id;
    int score;
} student_t;

// 冒泡排序所用比较函数,值为学生a的分数减去学生b的分数
int compare(const student_t *a, const student_t *b)
{
    return a->score - b->score;
}

// 冒泡排序的实现,传入student_t数组和数组大小n,以及比较函数的指针
void bubble_sort(student_t s[], int n, int *cmp(const void* a, const void* b))
{
    // 执行n-1趟
    for(int trips = 0; trips < n - 1, trips++)
    {
        // 将元素s[i]和元素s[i+1]做对比,i从0到n - trips - 1
        for(int i = 0; i < n - trips - 1; i++)
        {
            if(cmp(&s[i], &s[i+1]) > 0)
            {
                student_t temp = s[i];   // 可以直接使用swap(&s[i], &s[i+1])
                s[i] = s[i+1];
                s[i+1] = temp;
            }
        }
    }
}

// 二分查找,返回值为score的学生的index。但是只能查找到一个为score的学生
int binary_search(student_t s[], int n, int score)
{
    int l_index = 0, r_index = n - 1;
    while(l_index <= r_index)
    {
        int mid = (l_index + r_index) / 2;
        if(s[mid].score == score)
            return mid;
        else if(s[mid].score > score)
            l_index = mid + 1;
        else
            r_index = mid - 1;
    }
    return -1;
}

// 查找所有分数为score的学生,假设caller负责调用free函数
student_t *search_score(student_t s[], int n, int score, int* result_nums)
{
    int index = binary_search(s, n, socre);
    int l_index = index, r_index = index;
    *result_nums = 0;
    if(index < 0)
        return NULL;
    while(l_index > 0 && s[l_index - 1].score == score)
        l_index --;
    while(r_index < n - 1 && s[r_index + 1].score == score)
        r_index ++;
    *result_nums = r_index - l_index + 1;
    student_t *result = malloc(sizeof(student_t) * (*result_nums));
    memcpy(result, &s[l_index], sizeof(student_t) * (*result_nums));
    return result;
}

int main()
{
    int n = 100; // 给定学生数量
    int score = 89; // 目标分数
    // 假设题中所给学生数据由create_student_info提供。
    student_t *s_list = create_student_info(n);
    bubble_sort(s_list, n, compare);
	
    int result_nums = 0;
    student_t *result = search_score(s_list, n, score, &result_nums);
    for(int i = 0; i < result_nums; i++)
    {
        printf("name: %s, id: %d, score: %d", 
            (result + i) -> name, (result + i) -> id, (result + i) -> score);
    }
    return 0;
}

2.4 链表操作题(40分)

  1. 创建一个编号为 1 到 10 的链表
  2. 取m次范围为1-10的随机数,并:
    1. 将编号为m的链表移动到最前端
#include <stdio.h>
#include <time.h>

typedef struct node {
    int id;
    struct node* next;
} node_t;

int get_rand()
{
    return rand()%10;
}

// 传入链表和目标id;使用含头结点的链表
void move_node_to_head(node_t* nodelist, int id)
{
    node_t* pre = NULL;
    node_t* cur = *nodelist;

    // find target
    while(cur -> next && cur -> id != id)
    {
        pre = cur;
        cur = cur -> next;
    }

    // move node to head;
    if(cur -> id == id)
    {
        pre -> next = cur -> next;
        cur -> next = nodelist -> next;
        nodelist -> next = cur;
        return;
    } else {
        // 移动失败,在合法调用时也不存在该情况。
        return;
    }
}

node_t* create_list()
{
    node_t* head = calloc(sizeof(node_t), 1);
    node_t* curr = head;
    for(int i = 1; i <= 10; i++)
    {
        curr -> next = calloc(sizeof(node_t), 1);
        curr = curr -> next;
        curr -> id = i;
    }
    return head;
}

// 递归free链表
void destory_list(node_t* list)
{
    if(list != NULL)
    {
        destory_list(list -> next);
        free(list);
    }
}

int main()
{
    //设置随机数种子
    srand(time());
	
    int m = abs(rand());
	
    node_t* list = create_list();
	
    for(int i = 0; i < m; i++)
    {
        move_node_to_head(list, get_rand());
    }
	
    destory_list(list);
	
    return 0;
}

2019年计算机学院笔试题目

1. 改错题

是一个冒泡排序的改错。无题干信息。

2. 读程序和解答

2.1 辗转相除法求多个数的最小公倍数(重要、必背)

给定5个数,求这5个数的最小公倍数。

// 辗转相除法求最大公倍数,要求a 必须大于等于 b
int gcd(int a, int b)
{
    int mod = a % b;
    if(mod)
        return gcd(b, mod);
    else
        return b;
}

// 求两个数的最小公倍数
int lcm(int a, int b)
{
    if(a > b)
        return b * gcd(a, b);
    else
        return a * gcd(b, a);
}

// 求数组的最小公倍数,要求n>=2。
int LCM(int array[], int n)
{
    if(n < 2)
        return -1;

    // 先求前两个数的lcm
    int current_lcm = lcm(array[0], array[1]);

    // 求第三个数开始的lcm
    for(int i = 2; i < n; i++)
    {
        current_lcm = lcm(array[i], current_lcm);
    }
    return current_lcm;
}

2.2 字符串操作

在字符串 a 的指定位置 pos 插入字符串 b

#include <string.h>

int insert_str(char a[], const char b[], int pos)
{
    // 先计算字符串a、b的长度
    int a_len = strlen(a);
    int b_len = strlen(b);

    // 将a字符串的pos位后的字符串后移,由于目标内存和源内存可能存在内存区域重合,故只可使用 `memmove` 而不可使用 `strcpy` 。
    memmove(a + pos + strlen(b) * sizeof(char), a + pos, strlen(b) * sizeof(char));
    // 将字符串b拷贝到字符串a的指定位置
    memcpy(b, a + pos, strlen(b) * sizeof(char));
    return 0;
}

2.3 分析代码复杂度(重要)

给定三个函数,回答:

  1. 函数的功能
  2. 函数的时间复杂度
  3. 该方法存在的问题

函数1:

for(int i = 0; i < 100; i++)
{
	a[i] = rand() % 100 + 1;
	for(j = 0; j < i; j++)
		if(a[i] == a[j])
			i--;
}

函数2:

int tag[100] = { 0 };
for(int i = 0; i < 100; i++)
{
	a[i] = rand() % 100 + 1;
	if(tag[a[i]] == 1)
		i--;
	else
		tag[a[i]] = 1;
}

函数3:

for(int i = 0; i < n; i++)
{
	a[i] = i + 1;
	for(int j = 0; j < 100; j++)
		swap(&a[rand()%100], &a[rand()%100]);
}

函数1:

  1. 该函数的功能是向数组 a[100] 中输出值域为 [1, 100] 的随机数,并保证数组中没有重复元素。
  2. 该函数生成第 个元素所需要进行的元素对比次数为 次,故时间复杂度为
  3. 该函数所存在的问题是每进行一次生成都需要和前面所有已生成元素进行对比,效率较低。

函数2:

  1. 该函数的功能与函数1相同。
  2. 该函数生成第 个元素所需要生成随机数的次数的数学期望值为 故生成 个随机数的生成次数数学期望约为 次,故时间复杂度为
  3. 该函数所存在的问题是为避免生成重复的随机数多调用了 次随机数生成函数,但最后生成的数又都只是 之间的整数的集合,只保留了每个整数对应的随机位置信息,浪费了每个整数对应的数值信息。

函数3:

  1. 该函数的功能与函数1相同
  2. 该函数只对数组中 个元素进行了 次交换,时间复杂度为
  3. 该函数存在的问题是中间可能存在大量未被交换的元素,其元素值等于元素所在的位置。简要数学证明如下:
    1. 对于每个特定元素,每次交换被选中的概率为(减去两次选中的元素都是自己的情况):
    2. 共计进行100次交换,则该元素从始至终未被有效操作的概率为
    3. 则在100次交换后,该数列中未被操作过的元素数量的期望值为

2.4 求乱序数组的中位数

改错题,资料没给题干,故手写一遍。
先排序,再找中位数。
注意:

  1. 返回值应为float
  2. 当总大小为偶数时应返回两个数的均值
  3. 记得处理 n = 0 的情况。
int compare(int *a, int *b)
    return a - b;

float calc_mid_num(int array[], int n)
{
    qsort(array, sizeof(int), n, compare);
    if(n == 0)
        return -1;
    if(n % 2)
        return array[n/2];
    else
        return (array[n/2] + array[(n/2) - 1]);
}

3. 填空题(90分)

回忆版题目均未给空,以下内容均为从零手写

3.1 多元素综合排序

给定结构体 student ,其中存有 scoreageheight ,现在对其按照如下规则进行升序排序:

  1. 先比较 score
  2. score 相等时比较 age
  3. scoreage 均相等时比较 height
typedef struct student {
    int score;
    int age;
    int height;
} student_t;

int compare(student_t *a, student_t *b)
{
    if(a -> score != b -> score)
    {
        return a -> score - b -> score;
    } else if(a -> age != b -> age)
    {
        return a -> age - b -> age;
    } else {
        return a -> height - b -> height;
    }
}

int bubble_sort(student_t s[], int n, int (*cmp)(student_t *a, student_t *b))
{
    // 比较n-1趟
    for(int trips = 0; trips < n - 1; trips ++)
    {
        // 进行该趟的冒泡排序,将第i号引索元素和其后元素做对比,排序区间为 i \in [0, n - trips - 2]
        for(int i = 0; i < n - trips - 1; i ++)
        {
            if(cmp(&s[i], &s[i + 1]))
                swap(&s[i], &s[i + 1])
        }
    }
    return 0;
}

int sort_student(student_t s[], int n)
{
    return bubble_sort(s, n, compare);
}

3.2 删除字符串指定位置的指定长度的子串

// 删除字符串str的第pos位开始的长度为len的子串,pos从0计数,删除内容包含pos位
char * del_substr(char *str, int pos, int len)
{
    int str_len = strlen(str);
    if(pos > str_len)
    {
        return str;
    } else if(pos + len >= str_len) {
        memset(str, 0x00, str_len - pos);
        return str;
    } else {
        memset(str, 0x00, len);
        memmove(str + pos, str + pos + len, str_len - pos - len);
        memset(str + str_len - len, 0x00, len);
        return str;
    }
}

3.3 链表操作(较难,多练)

LeetCode 92
给定链表L,输入m,n。实现从链表的第m个节点到低n个节点逆序。

挺难,考虑类数组法。

typedef struct node
{
    int id;
    struct node* next;
} node_t;

// 无头结点的链表转置
node_t* reverse(node_t * const list)
{
    node_t *prev = NULL;
    node_t *curr = list;
    while(curr) {
        // 暂存下一节点
        node_t *next = curr -> next;
        // 将链表指针方向转置
        curr -> next = prev;
        // 移动窗口
        prev = curr;
        curr = next;
    }
    return prev;
}

// 翻转链表list的第start节点到第end节点的部分,使用带头结点的链表。
// start和end从1开始计算。
node_t* partical_reverse(node_t* const list, int start, int end)
{
    // 假设传入链表以及对应的start、end如下:
    // | 1 | -> ··· -> | a | -> | b | -> ··· -> | c | -> | d | -> ··· -> | e | -> NULL
    //   ↑                        ↑               ↑
    // list                     start            end
    // 则定义如下关系的指针:
    // | 1 | -> ··· -> | a | -> | b | -> ··· -> | c | -> | d | -> ··· -> | e | -> NULL
    //   ↑               ↑        ↑               ↑        ↑
    // list            prev     front            rear    after
    node_t *curr = list, *prev = NULL, *front = NULL, *rear = NULL, *after = NULL;

    // 随后获取上述指针的值,curr当作遍历指针
    int i = 0;
    while(curr)
    {
        // index从1开始
        i++;
        if(i == start - 1)
            prev = curr;
        if(i == start)
            front = curr;
        if(i == end)
            rear = curr;
        if(i == end + 1)
            after = curr;
        curr = curr -> next;
    }
    // 其中 prev 和 after 可能为 NULL 。

    // 截断 front 到 rear 的部分。
    rear -> next = NULL;
    reverse(front);

    // 拼回去
    front -> next = after;
    // 注意prev可能为null
    if(prev == NULL)
    {
        return rear;
    } else {
        prev -> next = rear;
        return list;
    }
}

3.4 数学计算

题目不全,假设要求写一个程序分别计算:
和:

double func1(int n)
{
	double sum = 0;
	for(int i = 1; i <= n; i++)
		sum += (2.0 * i - 1.0) / (2.0 * i + 1.0);
	return sum;
}

double func2(int n)
{
	double sum = 0;
	for(int i = 1; i <= n; i++)
		sum += (2.0 * i) / (2.0 * i + 1.0);
	return sum;
}

4. 简答题

4.1 C语言实现网络传输数据,应该使用TCP/IP协议还是HTTP协议,为什么

  1. HTTP是建立在TCP/IP上的一个应用层协议,其拥有TCP协议的所有功能,但是相对于TCP协议会较为复杂。
  2. 通常的网络数据传输使用TCP/IP协议更好一些,主要有以下考虑:
    1. 各类操作系统均内置了TCP/IP网络库而没有内置HTTP库,在开发难易性上TCP/IP占优。
    2. TCP/IP协议已经满足大部分程序间网络传输需求,无需使用更为复杂的HTTP协议。
  3. 但是依赖于HTTP传输的需求最好还是选择HTTP协议,避免重复造轮子,例如实现一个HTTP服务器或网页爬虫等。
  4. 综上所述,大部分简单的网络传输需求应该使用TCP协议,依赖于HTTP协议的需求应当使用HTTP协议,具体问题具体分析。

4.2 详细叙述一下一款客户定制的软件应该怎样进行测试,并交付使用(TODO)

5. 编程题

5.1 有序循环数组找最值(难)(TODO)

给定循环数组 Array[n] ,已知其有序,但不知其向哪有序,也不知队头在哪,求数组的最小值,并要求时间复杂度为否则不得分

Leetcode 128类似

程序思路:

  1. 先用O(1)的时间判断其有序方向,连续选取3个元素进行判断即可。
  2. 使用类似于二分查找的方法确定队头和队尾
  3. 再根据升降序进行判断

2018年计算机学院笔试题目(Finish)

1. 客观题

1.1 选择题(重点、难点)

求以下代码的输出:

void main()
{
	int a[] = { 1, 7, 12, 15 };
	int *p1 = a, *p2 = p1++;
	*p1 += *p2++;
	printf("%d %d", *p1, *p2);
}

注意点:

  1. *p2 = p1++ 是先赋值再自增
  2. 几个优先级问题:
    1. a = *p++ ,后置自增优先级比解引用高,等价于条目2
    2. a = *(p++) ,先对 p 解引用取值,随后 p 自增
    3. a = (*p)++ ,对 p 解引用,并对 p 指向的对象自增
void main()
{
	int a[] = { 1, 7, 12, 15 };
	int *p1 = a;
	*p2 = p1++;    // 此时p1指向a[1],p2指向a[0]
	*p1 += *p2++;  // a[1] += *p,p++
	printf("%d %d", *p1, *p2);
}

1.2 找错误

判断语句下列代码是否正确:

typedef struct
{
    int age;
    int score;
} student_t;

int main()
{
    student_t stu;
    student_t *p = &stu;
    *p.age = 23;
    return 0;
}

错误,因为 . 运算符取成员的优先级大于 * 解引用的优先级,故应更改为:

(*p).age = 23;

2. 改错题

错误点:

  1. 字符串占用的内存大小应当是 strlen(str) +1
  2. 字符串末尾要加 \0
  3. 指针赋值问题

3. 读程序的功能(难,多做)

要有耐心,算法可能不是普通人写的正常思路算法,不一定为较优解,故要慢慢猜。

#include <stdio.h>
void main()
{
	int i, j, k, d, flag1, flag2;
	scanf("%d", %d);
	printf("d = %d\n", d);
	for(i = 1; i <= 100; i++)
	{
		j = i;
		flag1 = 0;
		while((j > 0) && (!flag1))
		{
			k = j % 10;
			j = j / 10;
			if(k == d)
				flag1 = 1;
		}
		if(flag1)
		{
			j = i * i;
			flag2 = 0;
			while((j > 0) && (!flag2))
			{
				k = j % 10;
				j = j / 10;
				if(k == d)
					flag2 = 1;
			}
		}
		if(flag2)
			printf("%-5d %-5d\n", i, i * i);
	}
}

4. 简答题

4.1 简述C语言隐式转换会发生的四种情况,并说明每种如何转换

  1. 整数提升规则:较小的整数(如charshort)参与运算时,会被自动提升为 intunsigned int ,尽可能避免表达式中精度丢失问题。
  2. 算数类型转换规则:在有不同类型的操作数的表达式时,低精度的操作数会被隐式转换为高精度的操作数。
  3. 赋值转换规则:在进行赋值操作时,会隐式的提高或降低变量的类型,可能会丢失精度。
  4. 函数参数转换规则:当调用函数时,如果函数的参数类型与传递给它的实际参数的类型不匹配,则会进行函数参数转换。

4.2 如何将float类型四舍五入为long类型

有两种方法:

  1. 直接利用float转long的截断舍入规则:
long var = (long)(float_value + 0.5);
  1. 使用C11规范提供的 double round(double); 函数,先将float值进行四舍五入转换double的整数,随后利用截断赋值给long。该函数声明位于 math.h 但该函数在部分编译器(如GCC)下不支持。
long var = round(float_value);

4.3 请简述C语言采取了哪些措施提升其运行速度

见2023第一题:
1.1 简述一下C语言采取了哪些措施提高运行效率(10分)

1.1 简述一下C语言采取了哪些措施提高运行效率(10分)

  1. 允许程序直接访问内存,免去了各种抽象接口所造成的的额外开销。
  2. 使用指针特性,允许直接操作内存地址,可以直接访问内存中的数据,而无需进行额外的拷贝操作。
  3. 使用内联函数,允许编译器将部分函数代码直接插入到目标函数中,减少了函数调用的开销。
  4. 允许编译器对代码进行优化,使用重排、循环展开、函数内联等方式提高了生成的机器代码的执行效率。
  5. 提供低级语言特性,C语言提供了例如位操作、嵌入汇编等方式直接操作硬件,减少了代码的层次,提高了效率。
  6. 精简了标准库,减少了生成的机器代码的大小,提高了执行效率。
  7. 提供了宏功能(Macro),允许用户使用宏函数等方法减少了函数调用,将部分运算(例如sizeof)直接在预处理期完成。

4.4 数组越界会造成什么后果

数组越界是指访问数组时,访问到了该数组以外的内存空间,因此可以分为如下几类:

  1. 读取、写入到了该程序允许读写的内存空间:一般会导致未定义的后果,例如错误写入到别的变量的内存区域,会导致该变量值异常,并在后续触发对应后果,一般该后果难以预测。
  2. 写入到了该程序不允许写入的内存空间:一般将此类地址空间称为非法内存地址,会触发操作系统的异常中断,可能会导致操作系统弹出错误消息或者直接终止程序的执行。
  3. 在无虚拟内存的操作系统(如嵌入式常用的FreeRTOS)中,可能会操作到其他进程甚至系统进程的内存,会导致其他进程或系统异常并崩溃。
  4. 数组越界或指针经常会导致一些安全漏洞,攻击者可利用该特性绕过安全检查。

4.5 讲一讲函数的参数传递方式有哪些

  1. 值传递,在传递时拷贝一份副本给对应函数,在函数内对形参的操作不影响实参的值。
  2. 地址传递,传递时是传递了该参数的地址,是该参数地址的值传递。在C语言中通常通过传递数据的指针来达到直接修改参数减少大变量内存拷贝的消耗
  3. 引用传递:引用传递是C++中的特性,在C语言中并不支持。形参是实参的别名,指向同一地址,相比于地址传递多了类型检查(地址传递时不会检查改地址空间的参数类型是否为所要求的类型)。

4.6 讲一讲 "形参和实参都是数组元素" 、 "形参是指针,实参是数组地址" 、 "形参和实参都是数组地址" 时分别使用的是什么类型的参数传递。

  1. "形参和实参都是数组元素" 使用值传递
  2. "形参是指针,实参是数组地址" 使用地址传递,在函数传参时,数组会被当成指针处理。
  3. "形参和实参都是数组地址" 使用地址传递。

5. 填空题

5.1 快排找第k小

见2017年题目3.3 找第k大的数。

5.2 经典内存计算

做P267的2015年第4题。

5.3 Eratasthene筛选法求200以内的素数,修改代码使程序效率更高

原理:
原理

原理

既然是"筛选法",那他的核心思想就是从N个整数中逐个筛去合数,留下质数。
其原理为:

  1. 初始时先准备值为 以内的所有质数
  2. 依次删除 以内的上述质数的所有倍数
  3. 最终则会留下 以内的所有质数。
  4. 随后以 以内的所有质数做种,重复步骤1-4,直到找到 内所有质数。

原理优化

  1. 由于第四步 "重复步骤1-4" 时会反复筛选上一次已经筛干净的质数域,故从第二轮开始的步骤2中的 "" 无需从 开始,从 开始即可( 为上一次确定的质数范围)。

代码:
代码

代码

1
/**
2
* @brief: 求range以内的所有质数
3
* @param:
4
* bool *is_prime: 应传入的大小为range+1的bool数组,用于缓存结果
5
* int range: 求解质数的范围,其值应小于根号下的int值域上限(约46340)
6
**/
7
void Eratasthene(bool *is_prime, int range)
8
{
9
if(range < 2)
10
{
11
memset(is_prime, 0, (range + 1) * sizeof(bool));
12
return;
13
}
14
// 将所有标志位置1
15
memset(is_prime, 1, (range + 1) * sizeof(bool));
16
// 设置2以前的质数
17
is_prime[0] = false;
18
is_prime[1] = false;
19

20
// 当前可确定的质数范围
21
int scope = 2;
22
// 上一次确定的质数范围
23
int last_scope = 2;
24
while (last_scope * last_scope <= range)
25
{
26
for (int i = 2; i <= scope; i++)
27
{
28
if (is_prime[i])
29
{
30
// 进行质数筛
31
int multiple = i * i > scope ? i : scope / i;
32
int comp = multiple * i;
33
while ((comp = (multiple * i)) <= range)
34
{
35
is_prime[comp] = false;
36
multiple++;
37
}
38
}
39
}
40
last_scope = scope;
41
scope = scope * scope;
42
}
43
}

5.4 凯撒密码

题目要求:

  1. 该题目会通过主函数传入两个字符串,一个字符串是待加密的原文(只由字幕构成),一个字符串是字符串表示的整形变量(使用 atoi 函数)
  2. 完成凯撒密码的加密过程
凯撒密码原理:
原理

原理

凯撒密码的原理主要是将字母表向后或向前一定偏移量替换为密文字母表的一种加密方式。
Pasted image 20240319185350.png

实现:

void encrypt(char *str, char *key)
{
	int offset = atoi(key);  // atoi为stdlib.h中定义的将字符串转为整形的一个方法。
	offset %= 256;           // 保证offset范围为[0, 255]
	while(*str != '\0')
	{
		if('A' <= *str && *str <= 'Z')
		{
			// 此时为大写字符
			*str = (*str + offset) % 'A';
		} else if('a' <= *str && *str <= 'z'){
			// 此时为小写字符
			*str = (*str + offset) % 'a';
		}
		str++;
	}
}

6. 程序设计题(20分)

输入若干个整数,逆序构建双向链表。该整数序列以0结束。

typedef struct Node
{
	int value;
	struct Node* prev;
	struct Node* next;
} Node_t;

int verify(Node_t *node)
{
	int node_nums = 0;
	while(node)
	{
		node_nums++;
		printf("Index: %d, value: %d\r\n", node_nums, node -> value);
		node = node -> next;
	}
	return node_nums;
}

int main()
{
	int value = 0;
	Node_t *head = NULL;
	Node_t *rear = NULL;
	while(1)
	{
		scanf("%d", &value);
		if(value == 0)
			break;
		if(!head)
		{
			head = malloc(sizeof(Node_t));
			head -> value = value;
			head -> prev = NULL;
			head -> next = NULL;
			rear = head;
		} else {
			head -> prev = malloc(sizeof(Node_t));
			head -> prev -> next = head;
			head = head -> prev;
			head -> value = value;
			head -> prev = NULL;
		}
	}
	int node_nums = verify(head);
	printf("node nums: %d\r\n", node_nums);
	return 0;
}

2017年计算机学院笔试题目

1. 选择题

2. 改错题(20分)

3. 简答题(全同)

3.1 请简述C语言隐式转换会发生的四种情况,并说明每种如何转换。以及float如何四舍五入转化为int(同)

3.2 简述一下C语言采取了哪些措施提高运行效率(14分)(同)

3.4 变量以及内存布局问题(同)

4. 填空题

4.1 程序填空

求级数 项的值

double calculate(int n)
{
	// 直接计算第一项
	double sum = 1;
	// 每一项的阶乘分母
	double mul = 1;
	for(int i = 1; i <= n; i++)
	{
		mul *= i;
		double t = pow(-1, i) * pow(x, i) / mul;
		sum += t;
	}
	return sum;
}

4.2 完成完美乘法,a*b=c,abc中只出现0-9的数字,且每个数字最多出现一遍(TODO)

for(int i = 12; i < 999; i++)
{
	t = 0;
	for(x = 0; x < 10; x++)
		f[x] = 0;
	for(j = 345; j < 9999; j++)
	{
		__________;
		s[0] = a;
		s[1] = b;
		s[2] = c;
		for(x = 0; x < 3; x++)
		{
			y = s[x];
			__________ {
				int t = y % 10;
				f[t] ++;
				__________;
			}
			for(x = 0; x < 10; x++)
			{
				if(__________)
					t++;
			}
			__________;
			__________;
		}
	}
}

4.3 找第k大的数

题干:

int low = ___;
int high = ___;
do{
    i = low;
    _______;
    _______;
    do{
        _______;
        _______;
        _______;
    } while(___);
    _______;
    if(i == k)
        return ___;
    if(i > k)
        _______;
    if(i < k)
        _______;
} while(low < high);
return _______;

要点:

  1. 注意题目并没给函数体,故无法实现递归调用
  2. 题目并不要求处理后数列有序,因此多余的递归分支可以不要,直接将递归展开
  3. 参考答案中使用的数组index从0开始,记得从众,不然会判错。
  4. 他给出的空不可能完成该任务,不如和大家一起背答案...

答案:

int low = 0;
int high = n - 1;
do{
    i = low;
    j = high;
    pivot = array[i];
    do{
        while(array[j--] > pivot);
        while(array[i++] < pivot);
        swap(&array[j], &array[i]);
    } while(i < j);
    array[i] = array[high];
    if(i == k)
        return array[i];
    if(i > k)
        high = i - 1;
    if(i < k)
        high = i + 1;
} while(low < high);
return a[low];

2015年计算机学院笔试题目

2. 变量及内存布局问题

要点:

  1. 内存按2Byte编址,则内存上每隔2个字节,内存地址才会+1
  2. 注意各类型变量大小
  3. 正确理解 "每个区域相对地址从0开始" 的条件
  4. 不要忘了函数的参数
  5. 所有立即数也算常量包括数组大小
  6. 该题表格也要会背,2017年并未给出表格,需要自己完成
  7. 可能要考虑内存对齐(反正偏移量一定为整数)
  8. 一定要反复检查各类型变量大小

题干:
根据下列代码,计算各区域变量的变量名、大小以及偏移量。其中 char 占2字节, int 占4字节,指针占2字节,内存按1字节编址。每个区域相对地址从0开始。

int num = 2;

void main()
{
	char str1[10] = { "UESTC" };
	char *str2 = "CHENGDU";
	static int year = 2015;
	char c;
}

void func(int m)
{
	int n = 10;
	return n;
}

则应当绘制如下表格:

内存区域 变量名或常量 占用大小 偏移量 备注(该列非答案)
常量区 2 4 0
10 4 2 数组大小也要计入常量
"UESTC" 12 4
"CHENGDU" 16 10
2015 4 18
10 4 20
全局变量区 num 4 0
静态变量区 year 4 0
main函数 str1 20 0
str2 4 10
c 2 12
func函数 m 4 0
n 4 2

注意变量存储区域类型

  1. 常量
  2. 全局变量
  3. 静态变量
  4. 各函数局部变量