数据结构与算法——从零开始学习(四)字符串和数组

版权声明:本文为博主csdn_aiyang原创文章,未经博主允许不得转载。 https://blog.csdn.net/csdn_aiyang/article/details/84959056

系列文章

第一章:基础知识

第二章:线性表

第三章:栈和队列 

第四章:字符串和数组

第五章:树和二叉树

第六章:图

 


第四章:字符串和数组

第一节 :串

1.1  串的基本概念

1.2 串的基本运算

 

1.3 串的存储结构

第二节: 数组

2.1 数组的逻辑结构和基本操作

2.2 数组的存储结构

2.3 稀疏矩阵

本章总结


第四章:字符串和数组

字符串简称串,是一种特殊的线性表,其特殊性在于数据元素仅由一个个字符组成。作为一种基本数据类型,字符在计算机信息处理中意义非同一般,计算机非数值处理的对象经常是字符串数据。另外,串还具有自身的特性,常常把一个串作为一个整体来处理,因此,把串作为独立结构的概念加以研究是非常有必要的。本章简单介绍了串的存储结构及基本运算。

数组可视为线性表的推广,其特点是表中数据元素仍然是一个表。从本质上看,维数大于1的数组中数据元素之间不再是简单的一对一关系,因此,严格地说多维数组是非线性的。然而,由于数组中数据元素类型的一致性和其内部结构上的同一性,在实际处理数组时可以借助线性表的方法来实现数组及其运算。本章将会介绍数组的逻辑结构和存储结构、稀疏矩阵及其压缩存储等内容。

第一节 :串

1.1  串的基本概念

串(String)是由零个或多个任意字符串组成的字符序列。记做:s ="a1a2··an",其中,s是串名。a1(1<=i <=n)是一个任意字符,i是该元素在整个串中的序号;n为串的长度,表示串中所包含的字符个数,当n=0时,称为空串。

子串和主串:串中任意连续的字符组成的子序列称为该串的子串;包含子串的串相应地称为主串。

子串的位置:子串的第一个字符在主串中的序号称为子串在主串中的位置。

串相等:若两个串的长度相等且每一个对应字符都相等,就称这两个串是相等的。

 

1.2 串的基本运算

  1. 求串长:StrLength(s);
  2. 串赋值:StrAssign(s1,s2);    // 将s2的串值赋予s1
  3. 连接运算:StrConcat(s1,s2,s) 或 StrConcat(s1,s2).//在s1后面连接s2的串值,产生新串s
  4. 求子串:SubStr(s,i,len);//返回s的第i至len个字符的子串值。len=0为空串。例如:SubStr("abcdefg",2,3) = "bcd"
  5. 串比较:StrComp(s1,s2);//若s1 =s2 ,操作返回值为0;s1<s2,返回值<0;反之>0
  6. 串定位:StrIndex(s,t);//若t被包含于s中,则返回值为t的位置,反之值为-1
  7. 串插入:StrInsert(s,i,t);//将串t插入到s的第i个字符位置上
  8. 串删除:StrDelete(s,i,len);//删除串t中第i至len个字符的子串
  9. 串修改:StrRep(s,t,r);//用串r替换s中出现的所有与串t相等且不重叠的子串

 

1.3 串的存储结构

因为串是数据元素类型为字符的线性表,所有线性表的存储方式仍适用于串,也因为字符的特殊性和字符串经常作为一个整体来处理的特点,串在存储时还有一些与一般线性表不同的地方。

1、串的定长顺序存储结构:

类似于顺序表,可以用一组地址连续的存储单元存储串值中的字符串序列,所谓定长是指按预定义的大小为每一个串变量分配固定长度的存储区。如下,串的最大长度不能超过256。

#define MAXSIZE 256
char s[MAXSIZE]

标识实际长度的常用方法有三种:

第一种:类似顺序表,用一个指针来指向最后一个字符,这样表示的串描述如下:

typedef struct{
    char data[MAXSIZE];
    int curlen;
}SeqString;

SeqString s;//串变量

这种存储方式可以直接得到串的长度:s.curlen+1 。

第二种:在串尾存储一个不会在串中出现的特殊字符串作为串的终结符,以此表示串的结尾。例如,C语言中处理定长串的方法就是这一点,它用“\0”来表示串的结束。这种存储方法不能直接得到串的长度,根据当前字符是否是“\0”来确定串是否结束,从而计算出串的长度。

第三种:设定串长存储空间:chars[MAXSIZE+1],用s[0]来存放串实际长度,而串值存放在s[1]~s[MAXSIZE]中,字符的序号和存储位置一致,应用更为方便。


2、堆分配存储结构

在顺序串上的插入、删除操作并不方便,必须移动大量的字符,而且当操作中出现串值序列的长度超过上界MAXSIZE时,只能用截尾法处理。要克服这个弊病,只有不限定串的最大长度,动态分配串值的存储空间。

堆分配存储结构的特点是:仍以一组地址连续的存储单元存放串的字符序列,但其存储空间是在算法执行过程中动态分配得到的。在C语言中,由动态分配函数malloc()和free()来管理。利用函数malloc()为每一个新产生的串分配一块实际需要的存储空间,若分配成功,则返回一个指针,指向串的起始地址。串的对分配存储结构如下:

typedef struct{
    char *ch;
    int len;
 }HSTRING:

由于堆分配存储结构的串既有顺序存储结构的特点,在操作中又没有串长的限制,显得很灵活,因此,在串处理的应用程序中常被选用。

3、定长顺序串基本运算的实现

串连接:把两个串s1和s2首尾连接成一个新串s,即s<-s1+s2

int StrConcat(s1,s2,s)
    char s1[],s2[],s[];//将串s1,s2合并到串s,合并成功返回1,否则返回0
{
    int i =0,j,len1,len2;
    len1 = StrLength(s1);
    len2 = StrLength(s2);
    
    if(len1+len2>MAXSIZE-1) return 0;    //s 长度不够

    j = 0 ;
    while(s1[j]! ="\0"){
        s[i] = s1[j];
        i++;
        j++;
    }
     while(s2[j]! ="\0"){
        s[i] = s2[j];
        i++;
        j++;
    }
    s[i] = "\0";
    return 1;
}

求子串:

int StrSub(char *t ,char *s,int i , in len){
//用t返回串s中第i个字符串开始的长度为len的子串,1<=i串长
    
    int slen;
    slen = StrLength(s);
    if(i<1 || i>slen || len<0 || len>slen-i+1){
        return 0;
    }
    
    for(j =0 ; j<len ; j++){
        t[j] =s[i+j-1];
        t[j] ="\0";
        return 1;
    }
}

串比较:

int StrComp(char *s1 ,char *s2){
    int i =0;
    while(s1[i] == s2[i] && s1[i]!="\0"){
        i++;
    }
    return (s1[i] -s2[i]);
}

串定位:

int StrIndex(char *s ,char *t)
//返回子串t在主串s中的位置,若不存在则返回-1
{
    int i=0, j = 0;
    while(s[i] !="\0" && t[j] !="\0"){

        if(s[i] == t[j]){//匹配成功,继续比较下一个字符
            ++i;
            ++j;
        }else{         //否则主串换一个起始位置,子串重0开始
            i = i-j+1;
            j =0;
        }
        
        if( t[j] == "\0") { //匹配成功,返回匹配的第一个字符位置
            return i -j;
        }else{            
            return -1;
        }
    }

子串的定位操作通常称做串的模式匹配,是各种串处理系统中最重要的操作之一。上面的算法是一种简单的带回溯的匹配算法,该算法思路比较简单,容易理解,但其视觉复杂度较高,最坏情况下为O(slen*slen)。

第二节: 数组

2.1 数组的逻辑结构和基本操作

数组(Array)是一种数据结构,高级语言一般都支持数组这种数据类型。特点是结构中的元素本身可以是具有某种结构的数据,但属于同一数据类型。从逻辑结构上,可以把数组看做一般线性表的扩充。例如,一维数组就是一个线性表,二维数组就是"数据元素是一维数组"的一维数组。以此类推,即可得到多维数组的定义。

如有一个m行n列的二维数组:

可以把二维数组看成是一个线性表:A=(a1,a2····,an),其中aj(1<=j<=n)本身也是一个线性表,称为列向量(Column Vector),即aj=(a1j,a2j···,amj)。同样还可以将数组A看成另外一个线性表:B={B1,B2,···,Bm),其中Bi(1<=i<=n)本身也是一个线性表,称为行向量(Row Vector),即Bi=(ai1,ai2,····aim)。

在二维数组中,元素aij处在第i行和第j列的交叉处,即元素aij同时有两个线性关系约束,aij既是同行元素aij-1 的“行后继”,又是同列元素ai-1j的“列后继”,又是同列元素ai-1j的“列后继”。同理,三维数组可以看成这样的一个线性表,即其中每个数据元素均是一个二维数组,即三维数组中每个元素同时有三个线性关系约束,推广之,n维数组就是“数据元素为n-1维数组”的线性表。

由数组的结果可以看出,数组中的每一个元素由一个值和一组下表来描述。值表示数组中元素的数据信息,下标用来描述该元素咋数组中的相对位置。数组的维数不同,描述其相对位置的下标的个数也不同。例如,在二维数组中,元素aij由两个下标i、j来描述,其中i表示该元素的行号,j表示该元素的列号。

数组是一个具有固定格式和数量的数据有序集,即,一旦定义了数组的维数和每维的上、下限,数组的元素个数就固定了,而且数组中的每一个元素也由唯一的一组下标来标识。因此,在数组上一般不能做插入、删除数据元素的操作。对数组的操作通常只有下面两类。

(1)取值操作:给定一组下标,读其对应的数据元素。

(2)赋值操作:给定一组下标,存储或修改与其相对应的数据元素。

因此,数组的操作注意是数据元素的定位,即给定元素的下标,得到该元素在计算机中的存储位置。其本质就是地址计算问题。接下来以二维数组展开说明,因为二维数组是应用最广泛的,也是最基本的,对于大于二维的多维数组的存储和操作方法可以类推。

 

2.2 数组的存储结构

由于数组的特点是数组中数据元素的个数固定且其结构不变化,数组操作基本就是取值、赋值运算,因此,对于数组而言,采用顺序存储结构表示比较合适。对于一维数组可以直接按其下标顺序分配内存空间;而对于多维数组,必须按某种次序将数组中元素排成一个线性序列,然后按该序列将数据元素存放在一维的内存空间中。

存储二维数组时,一般有两种存储方式:第一种是以行序为主序(先行后列)的顺序存储方式,即从第一行开始存放,一行存放完了接着存放下一行,直到最后一行为止;另一种是以列序为主序(先列后行)的顺序存储方式,即一列一列的存储。

以行序为主序的存储分配的规律是:最右边的下标先变化,即最右下标从小到大,循环一遍后,右边第二个下标再变,···,从右向左,最后是左下标。以列序为主序存储分配的规律恰好相反:最左边的下标先变化,即最左下标从小到大,循环一遍后,左边第二个下标再变,···,从左向右,最后是右下标。例如,一个2X3的二维数组,以行序为主序的分配顺序为:a1,a2,a3 | a4,a5,a6  ;以列序为主序的分配顺序为:a1,a4 | a2 ,a5| a3,a6 。

设有m × n 二维数组Amn ,按元素的下标求存储地址:

以行序为主序为例:设数组的基址为LOC(a11),每个数组元素占据L个地址单元,那么aij的物理地址可用一线性寻址函数计算:LOC(aij) = LOC(a11)+((i-1)× n+j-1) ×L 。因为数组元素aij的前面有i-1行,每一行的元素个数为n,在第i行中它的前面还有j-1个数组元素。在C语言中,数组中每一维的下届定义为0,则:LOC(aij) = LOC(a00) +(i×n+j)×L。

推广到一般二维数组A[c1···d1][c2···d2],则aij的物理地址计算函数为:LOC(aij) = LOC(ac1c2) +((i-c1) ×(d2-c2+1)+(j-c2))×L。同理,对于三维数组Amnp,即m×n×p数组,数组元素aijk的物理地址为:LOC(aijk) = LOC(a111)+((i-1)×n×p+(j-1)×p+k-1)×L。

 

2.3 稀疏矩阵

稀疏矩阵(Sparse Matrix)是指矩阵中大多数元素为零元素的矩阵,即设m×n矩阵中有t个非零元素且t<<m×n ,则称之为稀疏矩阵。很多科学管理及工程计算中,常会遇到阶数很高的大型稀疏矩阵。如果按常规分配方法,顺序分配在计算机内,那将是相当浪费内存的。为此提出另外一种存储方法,仅存放非零元素。但对于这类矩阵,通常零元素分布没有规律,为了能找到相应的元素,仅存储非零元素的值是不够的,还要记下它所在的行和列。于是采取如下方法:非零元素所在的行、列及它的值构成一个三元组(i,j,v),然后按某种规律存储这些三元组,这种方法可以大大节约存储空间。

1、稀疏矩阵的三元组表存储

将三元组按行优先的顺序,同一行中列号从小到大的规律排列成一个线性表,称为三元组表,采用顺序存储方法存储该表。如下图:

这种存储结构的具体实现如下:

#define SMAX 1024 //足够大的空间
typedef struct{
    int i ,j ; //非零元素的行、列
    datatype v; //非零元素值
}SPNode;     //三元组类型

typedef struct{
    int mu,nu,tu; //行列及非零元素个数
    SPNode data[SMAX];//三元组表
}SPMatrix; //三元组表的存储类型

//定义一个稀疏矩阵的变量:
SPMatrix M;

稀疏矩阵的转置运算:

设A为一个m×n的稀疏矩阵,则其转置矩阵B就是一个n×m的稀疏矩阵,因此它们可以采用相同的数据类型,即:

SPMatrix A,B;

转置运算需要完成的工作包括:A的行、列分别转化成B的列、行;将A.data中每一个三元组的行与列交换后复制到B.data中。以上两点完成之后,似乎完成了B,但实际上没有。因为前面规定的三元组表是按行从小到大且同一行中的元素按列号从小到大的规律顺序存放的,因此转置后的矩阵B也必须按此规律排列。算法思路如下:

(1)A的行、列转行成B的列、行;

(2)在A.data中依次找第一列的、第二列的直到最后一列的三元组,并将找到的每个三元组的行、列交换后顺序存储到B.data中即可。

void TransM1(SPMatrix *A){
    SPMatrix *B;
    int p,q,col;
    B = malloc(sizeof(SPMatrix));//申请存储空间
    B ->mu =A ->nu; 
    B ->nu =A ->mu;
    B ->tu =A ->tu;//稀疏矩阵的行、列、元素个数
    if(B->tu>0){
        q=0;
        for(col =1 ;col <=(A->nu);col++){//扫描整个三元组数
            for(p=1;p<(A->nu);col++){
                if(A->data[p].j == col){
                    B->data[q].i = A ->data[p].j;
                    B->data[q].j = A ->data[p].i;
                    B->data[q].v = A ->data[p].v;
                    q++;
                }
            }
        }
    }
    if(B->tu > 0){
        return B;
    }
}
    

分析该算法,其时间主要耗费在col和p的二重循环上,所以时间复杂性为O(n×t)(设m、n是原矩阵的行、列数,他是稀疏矩阵的非零元素个数),显然,当非零元素的个数t和m×n同数量级时,算法的时间复杂度为O(m×n²),和通常存储方式下矩阵转置算法相比,可能节约了一定量的存储空间,但算法的时间复杂度更差了一些。

算法改进:上面算法低效率的原因是算法要从A的三元组表中寻找第一列、第二列、···,要反复查找A,若能直接确定A中每一三元组在B中的位置,则对A的三元组表扫描一次即可。这是可以做到的,因为A中第一列的第一个非零元素一定存储在B.data[1]中,如果还知道第一列的非零元素的个数,那么第二列的第一个非零元素在B.data中的位置便等于第一列的第一个非零元素在B.data中位置加上第一列的非零元素的个数。以此类推,因为A中三元组的存放顺序是先行后列,对同一行来说,必定先遇到列号小的元素,这样只需扫描一遍A.data即可。

  根据上面的想法,需要引入两个向量来实现:num[n+1]和cpot[n+1],num[col]表示矩阵A中第col列的非零元素的个数(为了方便均从1单元用起),cpot[col]初始值表示矩阵A中的第col列的第一个非零元素在B.data中位置。于是cpot的初始值为:cpot[1]  =1 ;cpot[col] = cpot[col-1]+num[col -1]; 2<=col<=n 。

SPMatrix *TransM2(SPMatrix *A){
    SPMatrix *B;
    int i,j,l;
    int num[n+1],cpot[n+1];
    B = malloc(sizeof(SPMatrix));//申请空间
    //稀疏矩阵行列元素个数
    B->mu = A ->nu;
    B->nu =A ->mu;
    B ->tu =A ->tu;
    if(B->to > 0){
        for(i=1;i<=A->nu;i++){
            num[i] = 0;
         }
        for(i=1 ;i<=A ->tu ;i++){
             j=A->data[i].j;
             num[j]++;
         }
        cpot[1] =1; //求矩阵A中每一列第一个非零元素在B.data中的位置
        for(i =2 ;i<A->nu;i++){
            cpot[i] =cpot[i-1]+num[i-1];
        }
        for(i =1 ;i<(A->tu);i++){ // 扫描三元组表
            j =A->data[i].j;
            k =cpot[j];
            B->data[k].i = A->data[i].j;
            B->data[k].j = A ->data[i].i;
            B->data[k].v = A->data[i].v;
        }
    return B;
}

分析这个算法的时间复杂度:这个算法中有四个循环,分别执行了n、t、n-1、t次,在每一个循环中,每次迭代的实际是一个常量,因此总的时间复杂度是O(n+t)。当然,它所需要的存储空间比前一个算法多了两个向量的存储空间。

 

本章总结

(1)字符串作为一种线性表,其特殊性在于表中每个元素就是单个字符。事实上,许多高级语言都提高了字符串操作的基本功能,在此希望肚子通过对本章的学习,了解各种程序设计语言中在实现字符串操作时的具体实现方法。

(2)理解数组的特点:1,n维数组可看成是这样一个线性表,其中每个元素均是一个n-1维的数组;2,数组是一组有固定个数的元素的集合,即给出数组的维数和每一维的上下界,数组中的元素个数就固定了;3,数组采用顺序存储结构,其主要操作是元素定位操作,因此要求掌握一维数组、二维数组的地址计算方法。

(3)稀疏矩阵是一种常见的数据结构,利用三元组表存储它的非零元素可由极大地压缩存储空间。

(4)在算法设计方面,要求掌握字符串的合并、数组中数据元素的原地逆置及矩阵的转置运算等。


系列文章:

数据结构与算法——从零开始学习(一)基础概念篇

数据结构与算法——从零开始学习(二)线性表

数据结构与算法——从零开始学习(三)栈和队列

猜你喜欢

转载自blog.csdn.net/csdn_aiyang/article/details/84959056