C语言算法与数组学习笔记
一、排序算法
1. 选择排序
• 核心思想:遍历数组,为每个位置选择当前剩余元素中的最小值(升序)或最大值(降序),通过交换实现排序。
• 代码实现:
for (int i = 0; i < n - 1; ++i) { // 外层循环控制排序位置(共n-1轮)
for (int j = i + 1; j < n; ++j) { // 内层循环寻找i位置之后的最小值
if (a[j] < a[i]) { // 升序比较(降序改为a[j] > a[i])
int temp = a[j];
a[j] = a[i];
a[i] = temp; // 交换元素位置
}
}
}
• 特点:每轮仅一次交换,空间复杂度低,但时间复杂度为 O(n²),适用于小规模数据。
2. 冒泡排序
• 核心思想:相邻元素两两比较,将较大(或较小)的元素逐步“冒泡”到数组一端,每轮确定一个最值。
• 代码实现:
for (int i = n - 1; i > 0; --i) { // 外层循环控制排序趟数(共n-1趟)
for (int j = 0; j < i; ++j) { // 内层循环完成每一趟的比较
if (a[j] > a[j + 1]) { // 升序比较(降序改为a[j] < a[j+1])
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp; // 交换相邻元素
}
}
}
• 特点:若某趟无交换,可提前终止(优化点),平均时间复杂度 O(n²),稳定性强。
3. 插入排序
• 核心思想:将数组分为“已排序”和“未排序”两部分,每次从未排序部分取元素插入到已排序部分的合适位置,类似抓牌排序。
• 代码实现:
for (int i = 1; i < len; ++i) { // 从第二个元素开始(第一个元素视为已排序)
int temp = a[i]; // 暂存当前待插入元素
int j = i;
while (j > 0 && a[j - 1] > temp) { // 向前查找插入位置(升序)
a[j] = a[j - 1]; // 后移元素
j--;
}
a[j] = temp; // 插入元素
}
• 特点:原地排序,空间复杂度 O(1),时间复杂度 O(n²),适用于近乎有序的数据。
二、查找算法:二分查找
• 前提条件:数组必须有序(升序或降序)。
• 核心思路:
1. 取数组中间元素与目标值比较;
2. 若目标值小于中间值,在左半部分继续查找;
3. 若目标值大于中间值,在右半部分继续查找;
4. 重复直至找到目标值或确定不存在。
• 代码示例(升序数组):
int binarySearch(int arr[], int n, int target) {
int left = 0, right = n - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // 计算中间下标(避免溢出)
if (arr[mid] == target) return mid;
else if (arr[mid] < target) left = mid + 1;
else right = mid - 1;
}
return -1; // 未找到
}
• 时间复杂度:O(log₂n),效率远高于线性查找。
三、字符型一维数组与字符串处理
1. 数组定义与初始化
• 定义格式:char 数组名[长度] = "字符串常量";
char str[10] = "hello"; // 自动添加结束符'\0',实际存储:h e l l o \0(长度6,字符串长度5)
• 存储特点:连续存放字符,以 \0(ASCII码0)作为字符串结束标志,不计入字符串长度。
2. 常用字符串操作函数
函数名 功能描述 示例代码
strlen 统计字符串长度(不含\0) int len = strlen("hello"); // len=5
strcpy 复制字符串(含\0) strcpy(dest, src);
strcat 拼接字符串(追加到\0位置) strcat(dest, src);
strcmp 比较字符串(字典序) int res = strcmp("abc", "abd"); // res=-1
3. 手动实现字符串复制
void my_strcpy(char *dest, const char *src) {
while (*src != '\0') { // 遍历源字符串直至'\0'
*dest = *src; // 复制字符
dest++; src++; // 指针后移
}
*dest = '\0'; // 补全目标字符串结束符
}
四、实战作业思路
1. 字符串倒置
• 思路:首尾指针相向移动,交换对应位置字符,直至指针相遇。
• 代码实现:
char str[100];
fgets(str, sizeof(str), stdin); // 安全输入字符串(避免越界)
int len = strlen(str);
for (int i = 0; i < len / 2; i++) {
char temp = str[i];
str[i] = str[len - 1 - i];
str[len - 1 - i] = temp;
}
printf("倒置后:%s\n", str);
2. 奇数下标字母转大写
• 思路:遍历字符串,判断奇数下标字符是否为小写字母,若是则转为大写(ASCII码减32)。
• 代码实现:
char ss[] = "hello123world";
int len = strlen(ss);
for (int i = 1; i < len; i += 2) { // i=1,3,5...(奇数下标)
if (ss[i] >= 'a' && ss[i] <= 'z') {
ss[i] -= 32; // 小写转大写
}
}
printf("处理后:%s\n", ss); // 输出:hEllO123WoRLd
五、总结
• 排序算法对比:选择排序、冒泡排序、插入排序均为 O(n²) 时间复杂度,适用于小规模数据;冒泡排序可通过优化提前终止,插入排序对有序数据更高效。
• 字符串处理核心:字符数组以\0结尾,操作时需注意边界避免越界,常用函数需熟练掌握底层逻辑(如strcpy的复制过程)。
• 算法应用场景:二分查找需先排序,适用于高频查询场景;字符串操作是文本处理的基础,需结合实际需求选择合适函数或手动实现。
补充1:如何理解记忆排序的程序?
三个排序算法记忆伪代码,突出核心逻辑和记忆关键点,用简洁符号和中文标注关键步骤:
一、选择排序记忆伪代码
|轮次|外层循环 i|内层循环找到最小值下标 min|交换前数组状态|交换后数组状态|
|---|---|---|---|---|
|第1轮|i=0|遍历 j=1~4,找到最小值 3(下标1)|[5,3,8,4,6]|[3,5,8,4,6](交换0和1位)|
|第2轮|i=1|遍历 j=2~4,找到最小值 4(下标3)|[3,5,8,4,6]|[3,4,8,5,6](交换1和3位)|
|第3轮|i=2|遍历 j=3~4,找到最小值 5(下标3)|[3,4,8,5,6]|[3,4,5,8,6](交换2和3位)|
|第4轮|i=3|遍历 j=4,找到最小值 6(下标4)|[3,4,5,8,6]|[3,4,5,6,8](交换3和4位)|
// 核心:每轮选最值放当前位
for i从0到n-2: // 外循环:n-1轮,定第i位
min = i // 假设i位是最小值
for j从i+1到n-1: // 内循环:找i后面的最小值
if a[j] < a[min] → min = j // 记录更小值下标
swap(a[i], a[min]) // 交换,最小值归位
// 记忆点:外定位置内找小,找到交换位置好
二、冒泡排序记忆伪代码
|趟数|外层循环 i|内层循环比较次数 j|每趟交换过程及数组状态变化|
|---|---|---|---|
|第1趟|i=4(n-1)|j=0~3|5>3 → 交换 [3,5,8,4,6]5<8 → 不换8>4 → 交换 [3,5,4,8,6]8>6 → 交换 [3,5,4,6,8]|
|第2趟|i=3(n-2)|j=0~2|3<5 → 不换5>4 → 交换 [3,4,5,6,8]5<6 → 不换|
|第3趟|i=2(n-3)|j=0~1|3<4 → 不换4<5 → 不换|
|第4趟|i=1(n-4)|j=0|3<4 → 不换(无交换,提前终止)|
// 核心:相邻比较,大的后移(像气泡上浮)
for i从n-1 downto 1: // 外循环:n-1趟,每趟沉一个大值到末尾
flag = 1 // 优化标志:无交换则提前停
for j从0到i-1: // 内循环:每趟比较前i个元素
if a[j] > a[j+1] → swap(a[j],a[j+1]) // 升序比较,大的后移
flag = 0 // 有交换则标记
if flag → break // 无交换,数组已有序
// 记忆点:外降趟数内升比,逆序交换泡上浮
三、插入排序记忆伪代码
|轮次|外层循环 i|待插入元素 temp|内层循环比较过程|插入后数组状态|
|---|---|---|---|---|
|第1轮|i=1(第2个元素)|temp=3|j=1,比较 a[0]=5>3 → 后移 a[1]=5j=0,停止 → 插入 a[0]=3|[3,5,8,4,6]|
|第2轮|i=2(第3个元素)|temp=8|j=2,比较 a[1]=5<8 → 停止 → 无需移动|[3,5,8,4,6]|
|第3轮|i=3(第4个元素)|temp=4|j=3,比较 a[2]=8>4 → 后移 a[3]=8j=2,比较 a[1]=5>4 → 后移 a[2]=5j=1,比较 a[0]=3<4 → 停止 → 插入 a[1]=4|[3,4,5,8,6]|
|第4轮|i=4(第5个元素)|temp=6|j=4,比较 a[3]=8>6 → 后移 a[4]=8j=3,比较 a[2]=5<6 → 停止 → 插入 a[3]=6|[3,4,5,6,8]|
// 核心:抓牌插入已排序区(类似扑克理牌)
for i从1到n-1: // 外循环:从第2个元素开始(第1个已有序)
temp = a[i] // 暂存当前待插入元素
j = i // 从当前位置向前找插入点
while j>0且a[j-1]>temp: // 内循环:后移比temp大的元素
a[j] = a[j-1]
j--
a[j] = temp // 插入到正确位置
// 记忆点:外取元素内后移,前大后移插空位
记忆强化技巧
1. 符号化关键操作:
◦ → 表示“执行”或“导致”,如 min = j → 记录最小值下标。
◦ swap() 用简短符号代替完整代码,聚焦逻辑。
2. 口诀辅助记忆:
◦ 选择排序:“外定位置内找小,找到交换位置好”。
◦ 冒泡排序:“外降趟数内升比,逆序交换泡上浮”。
◦ 插入排序:“外取元素内后移,前大后移插空位”。
3. 对比差异:
算法 外循环方向 内循环目的 核心操作
选择排序 从前往后(i++) 找最小值下标 交换当前位与最值
冒泡排序 从后往前(i--) 相邻比较,大值后移 交换相邻逆序元素
插入排序 从前往后(i++) 找插入位置,后移元素 暂存插入+后移
补充2:C 语言里,字符串是以 \0(ASCII 码 0) 作为结束标记的字符数组。printf("%s", ...) 会从字符串起始地址开始,逐个输出字符,直到遇到 \0 才停止。如果 \0 丢失、位置错误,或者数组后续内存有随机数据,就会出现乱码(输出不属于目标字符串的内容 )。
一、核心问题总结
C语言字符串输出乱码的核心原因是 结束标志\0异常,导致printf无法正确识别字符串终止位置,读取到无效内存数据。
二、常见原因与场景分析
1. 数组越界修改\0
• 场景:遍历字符串时下标超过有效长度(含\0),误修改\0字符。
char ss[] = "hello world"; // 有效字符11个(不含`\0`),总长度12(含`\0`)
int len = strlen(ss); // len=11(`\0`不计入)
// ❌ 错误:i <= len 会访问到 ss[11](即`\0`)
for (int i = 0; i <= len; i++) {
if (i % 2 == 1 && ss[i] >= 'a' && ss[i] <= 'z') {
ss[i] -= 32; // 可能将`\0`改为非0字符
}
}
• 后果:\0被破坏,printf持续读取后续内存随机数据。
2. 输入函数处理不当
• scanf截断带空格字符串:
char ss[100];
scanf("%s", ss); // 输入"hello world"时,仅存储"hello\0",后续字符丢失
• fgets未处理换行符:
char ss[100];
fgets(ss, sizeof(ss), stdin); // 输入后含换行符`\n`,未替换为`\0`
3. 手动构造字符串未补\0
• 场景:逐个字符赋值但未添加结束标志。
char ss[100];
int i = 0;
ss[i++] = 'h'; ss[i++] = 'e'; ss[i++] = 'l'; // 省略`\0`
ss[i++] = 'l'; ss[i++] = 'o'; ss[i++] = ' ';
ss[i++] = 'w'; ss[i++] = 'o'; ss[i++] = 'r';
ss[i++] = 'l'; ss[i++] = 'd'; // ❌ 无`\0`
三、解决方案与修正代码
1. 避免数组越界(以奇数下标转大写为例)
char ss[] = "hello world";
int len = strlen(ss); // len=11(有效字符数)
// ✅ 正确:i < len 仅处理有效字符,保留`\0`在ss[11]
for (int i = 0; i < len; i++) {
if (i % 2 == 1 && ss[i] >= 'a' && ss[i] <= 'z') {
ss[i] -= 32; // 仅修改有效字符
}
}
printf("处理后:%s\n", ss); // 输出正常
2. 正确处理输入字符串
• 使用fgets读取带空格字符串并处理换行符:
char ss[100];
fgets(ss, sizeof(ss), stdin); // 读取最多99个字符+`\0`
ss[strcspn(ss, "\n")] = '\0'; // 将换行符替换为`\0`
• 用scanf限制读取长度:
char ss[100];
scanf("%99s", ss); // 最多读取99个字符,确保`\0`存在
3. 手动构造字符串时补\0
char ss[100];
int i = 0;
ss[i++] = 'h'; ss[i++] = 'e'; ss[i++] = 'l';
ss[i++] = 'l'; ss[i++] = 'o'; ss[i++] = ' ';
ss[i++] = 'w'; ss[i++] = 'o'; ss[i++] = 'r';
ss[i++] = 'l'; ss[i++] = 'd';
ss[i] = '\0'; // ✅ 手动添加结束标志
4. 调试方法:检查\0位置
char ss[] = "hello world";
for (int i = 0; ; i++) {
printf("ss[%d] = '%c' (ASCII: %d)\n", i, ss[i], ss[i]);
if (ss[i] == '\0') break; // 正常应在i=11时输出ASCII: 0
}
四、总结
• 核心原因:\0丢失、位置错误或被修改,导致printf无法正确终止。
• 修复关键:
1. 确保数组操作下标不超过strlen(ss)(即不碰\0)。
2. 输入函数正确处理空格和换行符,保留\0。
3. 手动构造字符串必须以\0结尾。
• 调试建议:通过遍历字符和ASCII码,确认\0位置是否正确。