C语言-对多行文本的排序程序

对多行文本的排序程序

这里对Brian W. Kernighan和Dennis M. Richie的C语言经典教材The C Programming Language (Second Edition)中第五章(P93~P95)用于讲解指针数组的一段示例代码进行解读。

源程序的主要功能即:对长度不一的多个文本行按“字典顺序”进行排序(文本由用户输入到控制台),并将排序后的多行文本行再输出。

这里需要注意一个细节问题(实际上是主要问题):笔者在第一次看K&R的教材时,把所谓的“字典顺序”误认为是指按字符串的长度排序——实际上这两个概念大相径庭。但是为了排序的效果更加简明可视,我们对源程序稍加改进,还是按照不同文本行的长度来排序

问题分解

按照课本给出的思路,排序过程主要包括以下三个步骤,即输入排序输出三步。为了使思路更清晰,我们再将这三个主要步骤分为若干小步,并标明每一步需要用到的函数:

  1. 读取所有输入行–readlines函数 ;
    1.1: 获得一行输入并暂存–getline函数;
    1.2: 在栈中申请相应的空间–alloc函数;
    1.3: 将文本行从临时位置复制到安全位置(即栈中)储存–strcpy函数;
    1.4: 循环获取每一行。
  2. 对文本行进行排序(快速排序算法)–qsort函数;
    2.1: 选取中间元素作为“界”;
    2.2: 遍历指针数组,所指文本行长度小于“界”的指针被移到数组左半边–strcmp_len函数,用于比较两文本行长度,以及swap函数用于交换两指针在数组中的位置;
    2.3: 对左半边(较短的文本行)的集合再选取一个中间元素进行划分,右半边(较长的文本行)同理–递归使用qsort函数。
  3. 按次序打印文本行;
    3.1: 循环打印(已排序的)指针数组的每一个文本行。

1.读取部分

读取部分的主角是readlines函数,用于获得而缓冲区内输入的多行文本。但是解读readlines之前,我们还是最好先了解一下readlines中涉及到的几个而重要的函数,getline就是其一:


int getline(char s[], int lim)//读入数组s(用于储存文本)和每行最大字符数lim 
{
 int c, i;
 //lim-1是为了留一位给'\0'
 for (i=0; i<lim-1 && (c=getchar())!=EOF && c!='\n'; ++i)//i++和++i在这儿感觉都一样 
  s[i] = c;
 if (c == '\n'){//如果是因为c='/n'退出循环,此处再把/n记录下来 
  s[i] = c;
  ++i;
 }
 s[i] = '\0';//记录'\0'封口 
 return i;//返回除'\0'外字符数(也是'/0'在组中下标) 
}

getline函数接收一个数组名s和一个整型变量lim,前者用于暂存这行的字符流,后者用于限制每行允许获得的最大字符数。用于获得一整行的字符并(暂时)储存到一个字符数组中,获取字符直到遇到EOF(文本结束符号,实际上就是’\0’,用户可通过Ctrl + Z在控制台输入)或’\n’停止(’\n’也会被记录并算做一个字符)。

结束时,返回该行的总字符数i

alloc函数:

#define ALLOCSIZE 10000
static char allocbuf[ALLOCSIZE];//全局栈
static char *allocp = allocbuf; 

char *alloc(int n)//用于初始化空指针p(在固定栈allocbuf中为其申请n字节长度的空间) 
{
 if (allocbuf + ALLOCSIZE-allocp>=n){//allocp-allocbuf表示已占用空间,则左侧表达式判断剩余空间 
  allocp += n;//移动栈指针指向栈中下一个空位 
  return allocp - n;//返回指向申请的地址头部的 指针 
 }
 else
  return 0;//若空间不足,返回0(无效指针) 
} 

alloc函数是获得输入过程中很重要的一个函数,它接收一个整型变量n。功能即在栈allocbuf中申请n字节长度的空间,用于“永久”存放每行的字符。这么设计的主要原因是,getline函数的s[]数组在每轮循环都要用于存放新的一行,所以循环末尾必须把字符流由s中转移到一个安全的位置(这里即全局变量allobuf中),否则上一行的字符将不断被下一行覆盖。

值得一提的是,使用标准库中的malloc函数能够起到类似的作用,不过malloc函数返回的是不定对象指针void *p,需要再进行强制类型转换,比如这里可以是(char*)malloc(n)。

如果申请成功,alloc函数返回一个指向将要被用户使用的空间的头部的指针allocp-n;否则,返回0。(因为0作为指针即NULL,不可能是有效的地址)

int strcmp(char *s, char *t)

strcmp是标准库<string.h>中提供的一个字符串函数,用于将t指向的字符串(字符数组)复制到s所指的位置。在本例中,s即指向栈中地址的指针allocp-n。而如果直接采用s=t的方式赋值,只是把s指向了t所指的位置,这样,储存字符的位置仍然是危险的(会被覆盖)。

在了解了这些必要的工具之后,让我们开始解读readlines函数:

int readlines(char *lineptr[], int maxlines)
{
 int len, nlines;
 char *p, line[MAXLEN];
 nlines = 0;
 while ((len = getline(line, MAXLEN)) > 0)//除非字符数组line中只有EOF,否则循环获得下一行 
  if(nlines>=maxlines||(p=alloc(len))==NULL)//alloc函数用于空指针p的初始化 
   return -1;//储存空间不够时返回-1 
  else{
   line[len-1] = '\0';//删除换行符 
   strcpy(p, line);//将line中的字符全部转移到p所指的栈中,空置的line用于接收下一行的字符 
   lineptr[nlines++]=p;//实际上所有字符串都储存在p所指的栈中,指针数组lineptr只是相当于一个地址包 
  }
 return nlines;//成功储存则返回总行数 
}

readlines函数接收用于储存的指针数组名lineptr最大储存行数maxlines

主体思路就是:1.循环不断使用getline获得每一行输入,储存在临时位置line;2.调用alloc请求文本行长度len字节的空间,如果栈allocbuf中有空余位置,就返回p指向待使用地址的头部;3.采用strcpy将字符串从line位置复制到p所指的安全位置;4.并且将每一个p储存到指针数组lineptr中,方便后续访问;5.最后,返回接收的总行数nlines。

2.排序部分

排序部分的主体是qsort函数,但是与之前一样,让我们先来看一下qsort当中使用到的swap函数:

void swap(char*v[],int i,int j)//交换函数,接收指针数组和两个下标作为参数 
{
 char *temp;//指针temp在交换中用于暂存v[i] 
 //此时temp未初始化,有地址而无指向 
 temp = v[i]; 
 v[i] = v[j];
 v[j] = temp;//交换两个元素(指针)在数组中的位置 
}

swap函数的功能功能非常简单,就是用于按照要求交换数组中两个元素的位置。这里接收三个参数指针数组名v、2个需要进行交换的元素的索引i和j无输出

源程序用到的另外一个函数是strcmp

//strcmp函数:按s根据字典顺序小于、等于或大于t的结果分别返回负整数、0或正整数 
int strcmp(char *s, char *t)
{
 for (;*s=*t;s++,t++)
  if (*s == '\0')
   return 0;
  return *s-*t;
} 

而这里,为了完成比较长度的功能,我们编写一个strcmp_len函数:

//strcmp_len函数:按s所指字符串的长度小于、等于或大于t的结果分别返回负整数、0或正整数 
int strcmp_len(char *s, char *t)
{
return strlen(s)-strlen(t)

strcmp_len将比较接收的两个指针指向的字符串的长短,若s所指的字符串较t所指的更长,返回一个正整数;若两者一样长,返回0;若s所指的字符串更短,返回一个负整数。

接着,介绍qsort函数:

void qsort(char *v[], int left, int right)//快速排序函数,接收指针数组头v,集合左端元素索引left和集合右端元素索引right 
{
 int i, last;
 void swap(char *v[], int i, int j);
 
 if (left>=right)//如果集合中元素小于等于1,不用再排序 
     return;//直接退出   
 swap(v, left, (left+right)/2);
 //否则选定中间元素作为"界",置于数组0号
 last=left;
 //last置0,准备储存小于"界"的元素(此后last的值表示上一个小于"界"的元素储存的索引) 
 for(i=left+1;i<=right;i++)//从1号位(到right号位)开始检查元素 
  if(strcmp_len(v[i], v[left])<0){//如果有小于"界"的元素, 
      swap(v, ++last,i);//则储存到++last的位置 
  }
 swap(v, left, last);//检查完后,将中间元素归位
 //此时左边的元素全部小于"中界",右边的元素全部大于"中界",故可判断该中间元素位置已经正确 
 qsort(v, left, last-1);//递归,对左侧(较小)集合再进行排序 
 qsort(v, last+1, right);//...,对右侧(较大)...
}

这版的qsort函数采用“快速排序”算法, 由C. A. R. Hoare于1962年发明。具体步骤如下:

1.对于一个给定的数组,从中随机选取一个元素;
2.以这个元素为界,将其余元素划分为两个子集,一个子集中的元素全部小于该元素,另一个子集中的元素全都大于等于该元素;
3.对这两个子集递归执行这一过程,当某个子集的元素数目小于等于1时,便不用再对它排序(基例)。

在平均情况下该算法的时间复杂度为O(nlogn),比起O(n^2)的冒泡排序提升了不少。由于笔者最近在学习数据结构,这里再列出基于冒泡排序Shell排序(时间复杂度O(n^1.3))版本的qsort函数作为参考:

void qsort_bubble(char *v[], int left, int right)
{
 int i;
 int n = right-left+1;
 int sign = 0;
 for(;(sign = 1-sign);n--)
  for (i=0;i<n-1;i++)
   if (strcmp_len(v[i+1], v[i])<0){
    swap(v, i, i+1);
    sign = 0;
   } 
}
void qsort_Shell(char *v[], int left, int right)
{
 int i, j, gap;
 int n = right-left+1;
 char *temp=NULL;
 for (gap=n/2;gap>0;gap/=2)
  for(i=gap;i<n;i++)
   for(j=i-gap;(strcmp_len(v[j+gap], v[j])<0);j-=gap){
      temp=v[j];
      v[j]=v[j+gap];
      v[j+gap]=temp;
   } 
}

3.输出部分

输出部分的主体是writelines函数,思路非常简单;接收排序后的指针数组(名)lineptr和总行数nlines两个参数,无返回值。

void writelines(char *lineptr[], int nlines)
{
 while (nlines-- > 0)
 printf("%s\n", *lineptr++);  //*lineptr也是一个指针,初始指向第一行

//也可通过数组下标的形式实现遍历
// int i;
// for (i = 0; i < nlines ; i++)
//  printf("%s\n", lineptr[i]); 
}

4.主程序

依照先前的思路,main函数将依次调用readlines进行读取,调用qsort进行排序,最后调用writelines打印排序后的文本行:

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

#define MAXLINES 5000

char *lineptr[MAXLINES];

int readlines(char *lineptr[],int nlines);//读取文本函数 
void writelines(char *lineptr[], int nlines);//(按排好的顺序)打印文本函数 
void qsort(char *lineptr[], int left, int right);//排序函数 
void qsort_bubble(char *v[], int left, int right);
void qsort_Shell(char *v[], int left, int right);
int strcmp_len(char *s, char *t);//比较字符串长度函数

int main(){
 int nlines; 
 if ((nlines = readlines(lineptr, MAXLINES))>=0){//用nlines储存读取的总行数,若储存空间足够, 
  qsort(lineptr, 0, nlines-1);//则对读入的进行排序 
  writelines(lineptr, nlines);//打印排序后的文本行 
  return 0;
 } 
 else{
  printf("error: input too big to sort\n");//若储存空间不足,打印error提示 
  return 1; 
 }
}

这里顺便把头文件的引用、宏的定义、一些外部变量和函数的声明也写在一起,当然实际使用时,这段代码应该置于开头处。

总结

将以上四部分代码合并,就得到一个比较简陋的完整的多行文本排序程序了。采用键盘输入文本,回车换行并在下一行行首使用Ctrl + Z输入EOF符号(图中显示为^Z)后,再按回车以结束输入;输出时,文本行将按其长度以升序排列。

效果如下图:

在这里插入图片描述

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 要输出多行文本,可以使用C语言中的多行字符串常量。多行字符串常量使用一对双引号(")括起来,并以反斜杠(\)作为换行符。例如: ```c printf("这是第一行\n" "这是第二行\n" "这是第三行\n"); ``` 另一种方法是使用多个printf语句,每个语句输出一行文本。例如: ```c printf("这是第一行\n"); printf("这是第二行\n"); printf("这是第三行\n"); ``` 两种方法都可以输出多行文本,具体使用哪种取决于程序的需求。 ### 回答2: 在C语言中,可以使用双引号("")将多行字符串括起来,并使用反斜杠(\)在每行的末尾添加一个换行符(\n)来实现多行输出。 例如,我们想要输出以下多行字符串: Hello World C语言 我们可以使用以下代码实现: #include <stdio.h> int main() { printf("Hello\n"); printf("World\n"); printf("C语言\n"); return 0; } 这段代码中,我们使用了printf函数来输出字符串。每个printf函数调用都会输出一行字符串,并在末尾添加换行符,从而实现多行输出。 另外,如果要输出含有双引号的多行字符串,可以使用转义字符(\\)对双引号进行转义,以防止其被误解为字符串的结尾。 例如,如果要输出以下多行字符串: "This is a "quoted" string." 我们需要使用以下代码来转义双引号: #include <stdio.h> int main() { printf("This is a \"quoted\" string.\n"); return 0; } 通过使用\"来转义双引号,我们能够正常输出含有双引号的多行字符串。 ### 回答3: 在C语言中,要输出多行字符串,可以使用数组和循环来实现。以下是一种常用的方法: 1. 首先,定义一个字符数组来存储多行字符串,每行字符串使用双引号(")包围,且每行字符串后面加上转义字符(\n)代表换行。例如: ```c char multiple_lines[] = "第一行字符串\n" "第二行字符串\n" "第三行字符串"; ``` 这里的`multiple_lines`是一个存储多行字符串的字符数组。 2. 然后,使用循环遍历这个字符数组,并使用`printf`函数逐行输出字符串。例如: ```c #include <stdio.h> int main() { char multiple_lines[] = "第一行字符串\n" "第二行字符串\n" "第三行字符串"; int i; for (i = 0; multiple_lines[i] != '\0'; i++) { printf("%c", multiple_lines[i]); // 当遇到换行符时输出一个换行符 if (multiple_lines[i] == '\n') { printf("\n"); } } return 0; } ``` 在上述例子中,我们使用了`for`循环来遍历整个字符数组,然后在循环体中使用`printf`函数逐个输出字符,并在遇到换行符时输出一个额外的换行符。 通过以上的方法,就可以在C语言中实现多行字符串的输出。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值