PTA:使用指针进行字符串排序

使用指针方法输入3个字符串,将它们按从小到大的顺序输出。

输入格式:

在一行中给出3个长度为n字符串。
(1≤n≤1000)

输出格式:

在一行中,输出3个从小到大排序的字符串。

输入样例:

在这里给出一组输入。例如:

33
22
11

输出样例:

在这里给出相应的输出。例如:

11
22
33

代码 

#include 
#include 

#define MAX_LEN 1001

int main() {
    char str1[MAX_LEN], str2[MAX_LEN], str3[MAX_LEN];
    char *p1 = str1, *p2 = str2, *p3 = str3;

    // 指针方式输入
    scanf("%s", p1);
    scanf("%s", p2);
    scanf("%s", p3);

    // 创建指针数组
    char *strings[] = {p1, p2, p3};
    
    // 使用冒泡排序
    for (int i = 0; i < 2; i++) {
        for (int j = 0; j < 2 - i; j++) {
            if (strcmp(strings[j], strings[j+1]) > 0) {
                char *temp = strings[j];
                strings[j] = strings[j+1];
                strings[j+1] = temp;
            }
        }
    }

    // 输出结果
    for (int i = 0; i < 3; i++) {
        printf("%s\n", strings[i]);
    }

    return 0;
}

关键点说明:

  1. 指针方式输入
    通过将字符数组的指针(p1p2p3)传递给scanf,实现了题目要求的指针方式输入。

  2. 字符串比较与排序

    • 使用strcmp函数比较字符串字典序
    • 通过交换指针(而非复制字符串)实现高效排序
  3. 输入输出格式

    • 支持两种输入格式:
      同一行空格分隔(如33 22 11)或分三行输入(如样例形式)
    • 严格按题目要求每行输出一个字符串

 

代码功能概述

这段代码主要实现三个功能:

  1. 输入三个字符串(用指针方式访问)
  2. 对字符串进行排序(按字典序从小到大)
  3. 输出排序后的结果

代码逐行解析

1. 头文件与宏定义

c复制代码

#include 
#include 
#define MAX_LEN 1001
  • stdio.h:提供输入输出函数(如scanfprintf)。
  • string.h:提供字符串处理函数(如strcmp)。
  • MAX_LEN 1001:定义每个字符串的最大长度为1000字符(多1用于存储结束符\0)。

2. 主函数结构

c复制代码

int main() {
    // 变量定义
    char str1[MAX_LEN], str2[MAX_LEN], str3[MAX_LEN];
    char *p1 = str1, *p2 = str2, *p3 = str3;

    // 输入部分
    scanf("%s", p1);
    scanf("%s", p2);
    scanf("%s", p3);

    // 排序部分
    char *strings[] = {p1, p2, p3};
    // 冒泡排序逻辑...

    // 输出部分
    for (int i = 0; i < 3; i++) {
        printf("%s\n", strings[i]);
    }

    return 0;
}

3. 关键概念详解

(1)指针与字符串的关系
  • 字符数组str1str2str3 是三个长度为1001的字符数组,用于存储输入的字符串。

  • 指针初始化p1p2p3 是指向这三个数组的指针。

    c复制代码

    char *p1 = str1;  // p1指向str1数组的首地址
  • 输入方式scanf("%s", p1) 等价于 scanf("%s", str1),因为指针p1已经指向了str1的首地址。


(2)指针数组与排序
  • 指针数组char *strings[] = {p1, p2, p3} 创建一个包含三个指针的数组。

    • strings[0] 指向 str1
    • strings[1] 指向 str2
    • strings[2] 指向 str3
  • 排序目标:不修改字符串本身,而是通过交换指针的位置,让 strings[0]strings[1]strings[2] 按字典序从小到大指向字符串。


(3)冒泡排序逻辑

c复制代码

for (int i = 0; i < 2; i++) {
    for (int j = 0; j < 2 - i; j++) {
        if (strcmp(strings[j], strings[j+1]) > 0) {
            // 交换指针
            char *temp = strings[j];
            strings[j] = strings[j+1];
            strings[j+1] = temp;
        }
    }
}
  • 冒泡排序原理

    • 比较相邻元素,如果顺序错误就交换。
    • 每一轮将最大的元素“冒泡”到末尾。
  • 关键步骤

    1. strcmp(strings[j], strings[j+1]):比较两个字符串的字典序。
      • 返回值 > 0:表示 strings[j] 比 strings[j+1] 大。
    2. 交换指针:仅交换指针的指向,不复制字符串内容,提高效率。

4. 示例分析

假设输入:

复制代码

33
22
11
  • 初始状态

    c复制代码

    strings --> "33"
    strings[1] --> "22"
    strings[2] --> "11"
  • 第一轮排序(i=0):

    • 比较 strings[0] 和 strings[1]("33" vs "22"),交换后:

      c复制代码

      strings --> "22"
      strings[1] --> "33"
      strings[2] --> "11"
    • 比较 strings[1] 和 strings[2]("33" vs "11"),交换后:

      c复制代码

      strings --> "22"
      strings[1] --> "11"
      strings[2] --> "33"
  • 第二轮排序(i=1):

    • 比较 strings[0] 和 strings[1]("22" vs "11"),交换后:

      c复制代码

      strings --> "11"
      strings[1] --> "22"
      strings[2] --> "33"
  • 最终结果

    c复制代码

    strings --> "11"
    strings[1] --> "22"
    strings[2] --> "33"

5. 代码的巧妙之处

  1. 指针操作的高效性:通过交换指针(而不是复制字符串)完成排序,节省内存和时间。
  2. 字典序比较strcmp 函数直接比较字符串内容,无需手动逐个字符比较。
  3. 通用性:无论输入的字符串长度如何(只要 ≤1000),代码均可正确处理。

6. 常见疑问

为什么用 char *strings[] 而不是 char strings[][]
  • 如果使用二维数组(如 char strings[3][MAX_LEN]),交换字符串需要复制大量数据(时间复杂度高)。
  • 使用指针数组只需交换指针的值(时间复杂度低)。
为什么循环条件是 i < 2 和 j < 2 - i
  • 冒泡排序需要比较 n-1 轮(这里 n=3,所以 i 从0到1)。
  • 每轮比较次数递减(j < 2 - i),因为每一轮末尾的元素已经有序。

7. 进一步扩展

  • 动态内存分配:如果字符串数量不固定(比如由用户输入n个字符串),可以用 malloc 动态分配内存。
  • 其他排序算法:可以改用快速排序(更快)或选择排序(更简单)。

循环条件解析

外层循环:for (int i = 0; i < 2; i++)
  • 作用:控制排序轮数
  • 为什么是i < 2
    对于3个元素,只需要2轮比较即可完成排序。
    • 第1轮(i=0):确定最后一个元素的位置
    • 第2轮(i=1):确定中间元素的位置
    • 剩余的第1个元素自然就是最小的
内层循环:for (int j = 0; j < 2 - i; j++)
  • 作用:控制每轮比较的次数
  • 为什么是2 - i
    每完成一轮外层循环,末尾的元素已经正确排序,无需再比较。
    • 当i=0(第一轮)时,需要比较 2次(j=0和j=1)
    • 当i=1(第二轮)时,只需比较 1次(j=0)

排序过程演示(以输入 "33"、"22"、"11" 为例)

初始指针数组状态

复制代码

strings --> "33"
strings[1] --> "22"
strings[2] --> "11"

第一轮外层循环(i=0)
  1. 内层循环第一次比较(j=0)

    • 比较strings[0]strings[1]:"33" vs "22"
    • 因为strcmp("33","22")返回正数,交换指针:

      c复制代码

      strings --> "22"
      strings[1] --> "33"
      strings[2] --> "11" (未变)
  2. 内层循环第二次比较(j=1)

    • 比较strings[1]strings[2]:"33" vs "11"
    • 再次交换:

      c复制代码

      strings --> "22"
      strings[1] --> "11"
      strings[2] --> "33"

第二轮外层循环(i=1)
  1. 内层循环第一次比较(j=0)
    • 比较strings[0]strings[1]:"22" vs "11"
    • 交换:

      c复制代码

      strings --> "11"
      strings[1] --> "22"
      strings[2] --> "33" (已正确)

排序结果

复制代码

strings --> "11"
strings[1] --> "22"
strings[2] --> "33"

关键特性总结

特性 说明
时间复杂度 O(n²),但对3个元素只需2轮比较(实际比较次数:2 + 1 = 3次)
空间复杂度 O(1),仅用临时指针temp交换数据
排序稳定性 稳定排序(相等元素的相对顺序不变)
指针操作优势 只交换指针(4字节),不复制字符串内容(避免操作1000+长度的字符串)

对比其他排序方法

如果改用选择排序,核心逻辑会更清晰:

c复制代码

for (int i = 0; i < 2; i++) {
    int min_idx = i;
    for (int j = i+1; j < 3; j++) {
        if (strcmp(strings[j], strings[min_idx]) < 0) {
            min_idx = j;
        }
    }
    // 交换当前i位置与最小值位置的指针
    char *temp = strings[i];
    strings[i] = strings[min_idx];
    strings[min_idx] = temp;
}
  • 优势:减少不必要的交换次数
  • 劣势:逻辑稍复杂,需要维护min_idx 

一、冒泡排序的轮次规律

当需要排序的元素个数为 n 时,冒泡排序的轮次和比较次数遵循以下规律:

1. 轮次公式
  • 外层循环轮数n-1 轮
    • 每轮确定一个最大元素的位置(例如:3个元素需要2轮,4个元素需要3轮)
  • 内层循环每轮比较次数n-1 - i 次
    • i 是外层循环的当前轮次索引(从0开始)
2. 比较次数总和

总比较次数 = (n-1) + (n-2) + ... + 1 = n(n-1)/2 次
例如:

  • 3个元素 → 3次比较
  • 4个元素 → 6次比较
  • 5个元素 → 10次比较

二、冒泡排序分步解析(以4个元素为例)

假设数组初始顺序为:[4, 3, 2, 1]

外层循环第一轮(i=0)
内层循环轮次 比较元素 操作后数组状态
j=0 4 vs 3 → 交换 [3, 4, 2, 1]
j=1 4 vs 2 → 交换 [3, 2, 4, 1]
j=2 4 vs 1 → 交换 [3, 2, 1, 4]
结果:最大值4到达末尾
外层循环第二轮(i=1)
内层循环轮次 比较元素 操作后数组状态
j=0 3 vs 2 → 交换 [2, 3, 1, 4]
j=1 3 vs 1 → 交换 [2, 1, 3, 4]
结果:次大值3到达正确位置
外层循环第三轮(i=2)
内层循环轮次 比较元素 操作后数组状态
j=0 2 vs 1 → 交换 [1, 2, 3, 4]
结果:所有元素排序完成

三、更易理解的排序算法:选择排序

选择排序的核心思想是:每轮找到最小元素,放到已排序序列末尾。以下是代码实现:

c复制代码

void selectionSort(char *arr[], int n) {
    for (int i = 0; i < n-1; i++) {
        int min_idx = i;
        // 在未排序部分找最小值
        for (int j = i+1; j < n; j++) {
            if (strcmp(arr[j], arr[min_idx]) < 0) {
                min_idx = j;
            }
        }
        // 交换当前元素与最小值
        char *temp = arr[i];
        arr[i] = arr[min_idx];
        arr[min_idx] = temp;
    }
}
选择排序 vs 冒泡排序
特性 冒泡排序 选择排序
核心逻辑 相邻元素比较交换 扫描未排序部分找最小值
交换次数 多(可能每轮多次交换) 少(每轮只交换一次)
时间复杂度 O(n²)(无论数据是否有序) O(n²)
稳定性 稳定 不稳定(可能破坏相等元素顺序)
适用场景 小规模数据或基本有序的数据 小规模数据

四、用选择排序重写原题代码

c复制代码

#include 
#include 

#define MAX_LEN 1001

int main() {
    char str1[MAX_LEN], str2[MAX_LEN], str3[MAX_LEN];
    char *strings[] = {str1, str2, str3};

    // 输入
    for (int i = 0; i < 3; i++) {
        scanf("%s", strings[i]);
    }

    // 选择排序
    for (int i = 0; i < 2; i++) { // 只需比较前两个元素
        int min_idx = i;
        for (int j = i+1; j < 3; j++) {
            if (strcmp(strings[j], strings[min_idx]) < 0) {
                min_idx = j;
            }
        }
        // 交换指针
        char *temp = strings[i];
        strings[i] = strings[min_idx];
        strings[min_idx] = temp;
    }

    // 输出
    for (int i = 0; i < 3; i++) {
        printf("%s\n", strings[i]);
    }

    return 0;
}
选择排序执行流程

假设输入为 ["33", "22", "11"]

  1. 第一轮(i=0)

    • 找到最小值索引 min_idx=2(对应"11")
    • 交换 strings[0] 和 strings[2] → ["11", "22", "33"]
  2. 第二轮(i=1)

    • 在剩余元素中找到最小值 min_idx=1("22"已正确)
    • 无需交换

五、总结建议

  • 小规模数据:选择排序更直观,冒泡排序代码更简洁。
  • 理解排序本质:无论哪种算法,核心都是通过比较和交换将元素移动到正确位置。
  • 动手实践:用纸笔模拟不同输入下代码的执行过程(如输入 ["banana", "apple", "cherry"]),加深理解。

 

你可能感兴趣的:(数据结构,数据结构)