C++ 的编程模块

乐趣在于发现。


C++自带了一个包含函数的大型库,但真正的编程乐趣在于编写自己的函数。虽然语言越来越高级,C++也有强大的STL支持,但从创造到毁灭的乐趣才是无可替代的。

函数原型的功能

  1. 编译器正确处理函数的返回值
  2. 编译器检查使用的参数数目是否正确
  3. 编译器检查使用的参数类型是否正确

在C语言中,如果参数类型不正确则会导致错误,但是在C++中编译器会自动转化为正确的类型。(但是这样也会在函数重载中产生二义性),如果由大精度到小精度转换的时候编译器会发出警告,因此不要忽略任何一个警告。

有关数组方面,C++和C语言,将数组当指针处理。

至于数组和指针如何确定元素,《C++ PP》提出两个恒等式= =,

arr[i] == *(ar + i);

&arr[i] == ar + i;

tip:因为数组是指针传递,一定要注意数据保护,必要的时候可以使用const

对于程序设计,我们首先考虑的是通过数据类型和设计适当的函数处理数据,然后将这些函数组合成一个程序。有时也称为自下而上的程序设计,因为设计过程葱组件到整体进行。这种方法非常适合OPP,它首先强调的是数据表示和操纵。而传统的过程性编程倾向于从上而下的程序设计,首先制定模块化设计方案,然后再研究细节。这两种方法都很有用,优劣是相对而言的。最终的产品都是模块化程序。

对于把数组传参,一般有两种方法。

1.传递数组头地址和数组长度

2.传递数组头地址和尾地址。

第一种方法很常见,第二种方法挺有意思的。

对于double num[20];,我们可以传递num和num + 20,作为形参,指针num和num + 20定义了一个区间,是[num[0],num[19]],也就是说指向尾的指针是指向的最后一个元素的下一个元素(可能不存在)。

这样,对于数组遍历求和我们可以这样新颖的写:

for(pt = begin;pt != end;pt++)
total = total + *pt;

注意:根据指针减法规则,end-begin是一个整数值,等于数组的元素数目。

指针和const

指针和const的使用是一个很微妙的东西,可以有两种不同的方式将const关键之用于指针:

1.让指针指向一个常量对象,这样可以防止该指针修改所指向的值;

2.将指针本身声明为常量,可以防止改变指针指向的位置。

所有的操作,都是以这两个为基础的。

以前我们将常规变量的地址赋给常规指针,亦可以将常规变量的地址赋给指向const的指针(不可以用指针修改变量的值,但是可以用变量名修改)。因此还有两种可能性:

1.将const变量的地址赋给指向const的指针

2.将const的变量的地址赋给常规指针(非法)。

为什么第二种情况非法呢,因为常规指针是可以修改指向的值的,而指向的是一个const的值,那么编译器是不允许这么做的。

那么如果我们有一个const指针指向一个const常量,然后用一个指向指针指向这个const指针会如何呢?因为任何情况下编译器是允许将非const类型赋值给const指针的,因此描述二级间接关系,与一级间接关系一样将const和非const混合的指针赋值方式将不再安全。因此,仅当只有一层间接关系(如指针指向基本类型)时,才可以将非const地址或指针赋给const指针。

注意,如果数据类型本身并不是指针,则可以将const数据或非const数据的地址赋给指向const的指针,但只能将非const数据的地址赋给非const指针。

禁止将常量数据的地址赋给非常量指针将意味着不能讲数组名当做参数传递给使用非常量形参的函数。

因此,要尽可能的使用const

1.可以避免由于无意见修改数据而导致的编程错误

2.使用const使得函数可以处理const和非const实参,否则只能接受非const的数据。

如果条件允许,则应将指针形参声明为指向const的指针。

到底是指针是常量还是指针指向的常量呢?其实可以以为分界符,在左边有const就说明指向的内容不能改,在*右边就说明指针不能改,而左边的char和const顺序是不要紧的。

将指针作为函数形参来传递,可以使用指向const的指针保护。但是传递的必须是基本类型,比如

void show_array(const double ar[],int n);可以这样使用,但是如果这里的不是数组ar,而是指针甚至是指向指针的指针,不能保证只有一层的间接关系,则不能使用const。

对于函数和结构,要注意函数形参的特殊性,函数会把形参进行复制,如果结构过大,C语言一般采取传递地址的方法避免复制操作节省时间,而C++还支持引用。

函数和指针

指针和函数联用是个很高大上的东西,和数据一样,函数也是有地址的,这些地址对用户没有啥用,但是对于程序用处很大,可以编写将另一个函数地址作为参数的函数,这样第一个函数就能够找到第二个函数。使用起来将十分的灵活。

获取函数地址的方式和数组一样,函数名就是函数的地址。

对于声明函数指针,因为括号的运算级要高于*号,所以声明函数指针时要加括号,比如:double (*pf) (int);

其中pf便是指向形参为int,返回值为double的函数。

使用函数指针调用函数:

double pam(int);
double (*pf)(int);
pf = pam;
double x = pam(4);
double y = (*pf)(5);

//C++还允许这样使用
double y = pf(5);

虽然第一种方式不好看,但他明确的提示——正在使用函数指针。

函数指针的表示可以非常的恐怖,实现的功能也可以十分恐怖,比如用一个函数指针数组存储几个函数,用for循环依次执行这几个函数

但对于太恶心的指针可以使用auto,auto只允许但只初始化,不允许列表初始化。

自动类型推断确保变量的类型与赋给它的初值的类型一致,但一定要保证提供的初值的类型正确。


第七章终于写完了,要在一堆已经会的东西找出不太牢固的着实是个耐心活><,写到现在也是醉了。

2014/10/21 01:06 am posted in  C++

广义表

广义表是线性表的推广,也可称之为列表(lists)。广泛地用于人工智能等领域的表处理语言LISP语言,把广义表为基本的数据结构,就连程序也表示为一系列的广义表。

线性表被定义为一个有限的序列(a1,a2,a3,…,an)其中ai被限定为是单个数据元素。广义表也是n个数据元素d1,d2,d3,…,dn的有限序列,但不同的是,广义表中的di 则既可以是单个元素,还可以是一个广义表,通常记作:GL=(d1,d2,d3,…,dn)。GL是广义表的名字,通常广义表的名字用大写字母表示。n是广义表的长度。若其中di是一个广义表,则称di是广义表GL的子表。在广义表GL中,d1是广义表GL的表头,而广义表GL其余部分组成的表(d2,d3,…,dn)称为广义表的表尾。由此可见广义表的定义是递归定义的。因为在定义广义表时,又使用了广义表的概念。(百度百科,发现百度百科上就是抄的《数据结构》的原话嘛)

在广义表的定义中,元素a1可以是单个元素,也可以是广义表,分别称为广西表的原子子表

当广义表非空时,称第一个元素a1为广义表的表头,称其余元素组成的表是广义表的表尾。

广义表的定义是一个递归的定义,因为在描述广义表时又用到了广义表的概念。

根据定义,广义表的三条重要结论:

1.广义表的元素可以是子表,而子表的元素还可以是子表。由此,广义表是一个多层次结构;

2.广义表可为其他广义表共享(子表);

3.广义表可以是一个递归的表,即任何广义表可以是其本身的一个子表。

根据表头、表尾的定义可知:任何一个非空广义表其表头可能是原子,也可能是广义表,而其表尾必定为广义表。

注意:列表()和(())是不一样的,前者是空表,长度n=0,后者长度n=1,可分解得到其表头、表尾均为空表。即使成员表的长度为零,但因为有成员表,因此长度为1.这也体现了为什么说广义表是一个递归的表。

广义表的存储结构

由于广义表中的数据元素可以具有不同的结构(或原子或广义表),因此难以用顺序存储结构表示,通常采用链式存储结构,每个数据可用一个结点(还有升级空间啊,换上树的话鸟枪换炮啊)。

因为数据元素可能为原子或广义表,因此需要两种结构特点:一种是表结点,用以表示列表;一种是原子结点,用以表示原子。

反之,一对确定的表头表尾可唯一确定广义表。因此,一个表结点可由3个域组成:标志域(用枚举类型),指示表头的指针域和指示表尾的指针域;而原子结点只需两个域:标志域和值域。

模型如下:

typedef enum{ATOM,LIST}ElemTag; //ATOM == 0:原子,LISE == 1:子表
typedef struct GLNode{
    ElemTag tag;    //公共部分,用于区分原子结点和表结点
    union{          //原子结点和表结点的联合部分
        AtomType atom;  //atom是原子结点 的值域,AtomType由用户定义
        //ptr是表结点的指针域,ptr.hp和ptr.tp分别指向表头和表尾
        struct{struct GLNode *hp,*tp;}ptr;
    };
}*GList;  //广义表的类型

这种存储结构吧,是我挺喜欢的一种结构,因为第二种太丑了。

这种存储结构中:

1.除了空表的表头指针为空外,对任何非空列表,其表头指针均指向一个表结点,且该结点中的hp域指示列表表头(或为原子结点,或为表结点),tp域指向列表表尾(除非表尾为空,则指针为空,否则必为表结点);

2. 容易分清列表中原子列表中原子和子表所在层次。

3.最高层的表结点个数即为列表的长度。

还有另外一种结构,刚开始的时候我还没看出这两种结构的区别,仔细琢磨发现这两种结构的区别还是很明显的。

typedef enum{ATOM,LIST}ElemTag; //ATOM == 0:原子,LISE == 1:子表
typedef struct GLNode{
    ElemTag tag;    //公共部分,用于区分原子结点和表结点
    union{          //原子结点和表结点的联合部分
        AtomType atom;  //atom是原子结点 的值域,AtomType由用户定义
        struct GLNode *hp;  //表头指针
    };
    struct GLNode *hp;      //相当于next
}*GList;  //广义表的类型

看起来没有区别,但是画画图会发现这种存储结构相对离散。如果广义表中的子表需要较强独立访问的时候,还是可以考虑的。

m元多项式的表示

在第二章写线性表的时候就提到过如何存储多项式问题,在这里又提到了m元多项式的存储问题。在《数据结构》中,应用模型是实战的第一步,数据结构模型不是单纯的孤立存在,要有不太复杂的操作存在才有意义,才更有实用性。可以说一个模型是有存储,方法两部分组成的(好像C++的封装,不过这也就是ADT所在吧)。

在一般情况下使用的广义表多数既非是递归表,也不为其他表所共享。广义表中的一个数据元素可以是另一个广义表,这就为存储系数提供了便利。

其实m元多项式的表示和多项式的表示一样无语。其实就是先提取公因式把然后把系数保存下来。

步骤:

1.对一个字母提取公因式

2.分解出系数来依次保存(利用递归的特点)

3.向内递归(就是把系数再进行第一步操作)

typedef struct MPNode{
    ElemTag Tag;   //区分原子结点还是表结点
    int exp;    //指数域
    union{
        float coef //系数域
        struct MPNode *hp //表结点的表头指针
    };
    struct MPNode *tp //相当于next
}*MPList;

这个用的是第二种存储结构,虽然丑,但尽量保证的系数的独立性,在以后访问、计算速度可能会更棒一些。

在每一层上增设一个表头结构并利用exp指示该层的变元,可用一维数组存储多项式中所有的变元,故exp域存储的是该变元在一维数组的下标。

广义表的递归算法

鞠老师说这是这章最关键的地方,的确,非常有意思。

因为在做竞赛的时候学个一些树,对写一些简单的递归也算轻车熟路,这一节属于难点,但不是我的难点。大体总结之。

先说说递归,让我形容递归,就三个词:

1.慢:因为递归就是栈的应用,在使用栈空间时间是个问题,递归求阶乘就很好说明问题

2.不负责任:我写递归的时候一直处在不负责任状态,先写出来,管他对错呢,写完在想对错问题,= =。其实递归函数不好理解,等你想明白黄瓜菜都凉了,递归就要随心所欲,然后约束之。

3.强大:很多高级算法就是由递归写的,当递归看多了就发现,数学才是硬伤。

递归设计的实质是:当一个复杂的问题可以分解成若干子问题来处理时,其中某些子问题与原问题有相同的特征属性,则可利用和原问题相同的分析处理方法;反之,这些问题之间的转化关系。

由于递归函数的设计用的是归纳的思想,则在设计递归函数的时候注意:

1.严格定义功能和接口

2.把递归的每个操作看着简单操作

求广义表的深度

求广义表的深度和字典树很像,递归遇到子集深入。

复制广义表

因为广义表可以分为表头和表尾,那么只需要分别复制表头和表尾即可(有点分治的意思)。

if(L->tag==ATOM) T->atom = L->atom;
else{
    //分而治之
    CopyGList(T->ptr.hp,L->ptr.hp);
    CopyGList(T->ptr.tp,L->ptr.tp);
}

建立广义表的存储结构

因为前面已经提到过有两种存储结构,一种是把广义表分解为表头和表尾,另一种是把广义表看成含有n个并列子表(假设原子也视为子表)的表。其实递归存储和复制也差不到哪去。不过书中给出的代码的确不错

Status CreateGList(GList &L, SString S) {
  // 采用头尾链表存储结构,由广义表的书写形式串S创建广义表L。
  // 设emp="()"。
  char s[3]="()";
  SString emp;
  crt_SString(emp,s);
  SString sub,hsub;
  GList p,q;
  if (StrCompare(S, emp)) L = NULL;  // 创建空表
  else {
    if (!(L=(GList)malloc(sizeof(GLNode)))) return ERROR;  // 建表结点
    if (StrLength(S)==1) {  // 创建单原子广义表
       L->tag = ATOM;  L->atom =S[1]; }
    } else {
      L->tag = LIST;  p = L;
      SubString(sub, S, 2, StrLength(S)-2);  // 脱外层括号
      do {  // 重复建n个子表
        sever(sub, hsub);  // 从sub中分离出表头串 hsub
        CreateGList(p->ptr.hp, hsub);  q = p;
        if (!StrEmpty(sub)) {   // 表尾不空
          if (!(p = (GLNode *) malloc (sizeof(GLNode)))) return ERROR;
          p->tag = LIST;  q->ptr.tp = p;
        }//if
      } while (!StrEmpty(sub));
      q->ptr.tp = NULL;
    } // else
  } // else
  return OK;
} // CreateGList
2014/10/15 15:46 pm posted in  算法&数据结构

C++ 分支语句和逻辑运算符

第六章主要讲的是分支语句、逻辑运算符和文件读写入门,分支结构没有什么可以总结的,C++的分支结构和C完全一样。

但在逻辑运算符方面,C++居然支持直接用and,or,not来代替&&,||,!。也就是说,C++将前三个单词作为保留字(但不是关键字),不可以将前三个单词作为变量名。

C++在C上完美继承了字符处理的相关函数集,在C++中封装在cctype(原ctype.h)头文件。

其中常用函数:

函数名称 返回值
isalnum() 如果参数是字母数字,即字母或数字,该函数返回true
isalpha() 如果参数是字母,该函数返回真
isblank() 如果参数是空格或水平制表符,该函数返回true
iscntrl() 如果参数是控制字符,该函数返回true
isdigit() 如果参数是数字(0~9),该函数返回true
isgraph() 如果参数是除空格之外的打印字符,该函数返回true
islower() 如果参数是小写字母,该函数返回true
isprint() 如果参数是打印字符(包括空格),该函数返回true
ispunct() 如果参数是标点符号,该函数返回true
isspace() 如果参数是标准空白字符,如空格、进纸、换行符、回车、水平制表符或者垂直制表符,该函数返回true
isupper() 如果参数是大写字母,该函数返回true
isxdigit() 如果参数是十六进制的数字,即0~9、a~f、A~F,该函数返回true
tolower() 如果参数是大写字符,则返回其小写,否则返回该参数
toupper() 如果参数是小写字母,则返回其大写,否则返回该参数

文件输入输出入门

和C不一样,C++实现文件输入输出的方式几乎和对标准输入输出的方式一样。

C++通过对ostream类和istream类来实现标准输入输出的,而文件是依靠ofstream类和ifstream类来实现,并且包含在fstream头文件中。

我们可以自己定义ofstream类和ifstream类的对象,用成员函数open打开它,使用运算符>进行操作,用完后用close关闭它。

写入到文本文件

ofstream outFile;
ofstream fout;
outFile.open("text.txt");
char s[100];
cin >> s;
fout.open(s);
double data;
cin >> data;
outFile

应用起来十分方便,十分高大上。

文件输入输出可以使用标准输入输出的任何方法(我都怀疑文件输入输出是标准输入输出的继承),比如格式化方法如setf()和precision()。当然,这些函数只影响调用它们的对象。

在默认情况下,打开一个文件时发现文件不存在,则会新建这个文件,如果存在这个文件,则会先把文件清空,然后重新写(也就是清零了)。

读取文本文件

ifstream inFile;
ifstream fin;

inFile.open("text.txt");
char s[100];
cin >> s;
fin.open(s);

double data;
inFile >> data;

char line[80];
fin.getline(line.80);

一样的高大上。

如果文件打开失败,则is_open()返回TRUE,反之返回FALSE,因此可以判断是否成功打开了文件。

在文件操作头文件里,还定义了用于同操作系统通信的参考值EXIT_FAILURE,函数exit()终止程序。

if(!inFile.is_open())
{
exit(EXIT_FAILURE);
}

因为文件操作意外挺多,所有有个good类方法,来确定操作有木有成功。

所以建议每操作一次就检查一下good是否返回TRUE,

inFile >> value;

while(inFile.good())
{
inFile >> value;
}

因为inFile的返回值就是inFile.good()(和cin类似),所以可以简化:

while(inFile >> value)
{
;
}

2014/10/13 01:00 am posted in  C++

数组

数组不再是单纯的数据结构的原子类型,而可以形容为线性表的综合体,尤其像多维数组,更像其元素为线性表的线性表(如:二维数组可以看做每个元素是一位数组的一位数组,以此类推)。

数组一旦被定义,它的维数和维界就不再改变。因此,除了结构的初始化和销毁之外,数组只有存取元素和修改元素值的操作。

由于数组一般不做插入或和删除操作,也就是说,一旦建立了数组,则结构中的数据元素个数和元素之间的关系就不再发生变化。因此,采用顺序存数结构表示数组就是很自然的事情了。

由于存储单元是一维的结构,而数组是个多维的结构,则用一组连续存储单元存放数组的数据元素就有个次序约定问题。比如:二维数组要如何存储到线性的内存中?其实不同语言处理起来是不一样的。BASIC,PL/1,COBOC,PASCAL和C语言则采用了以行序为主序的存储结构,说白了,就是在线性内存中,先存储a00,...,a0n直到存储,an0,...,ann,而在FOR,TRAN等语言中,则一列序为主序,先存储a00,...,an0 直到存储 an0,...,ann。存储方式各有优劣,但对于高级语言来说,大部分时候不用考虑这些细节。

对于数组,一旦规定了它的维度和各维的长度,便可以为它分配存储空间,反之,只要给出一组下标,便可以求得相应数组元素的存储位置。

假设每个数据元素占L个存储单元,则二维数组A中任意一个元素aij的存储位置可由下式确定

LOC(i,j) = LOC(0,0)+(b2 x i + j)L

其中LOC(i,j)是aij的存储位置,LOC(0,0)是a00的存储位置,而二维数组A的起始位置,也称为基地址或基址,当然,这是二维数组,特殊到一半,便可得到n维数组的数据元存储位置的计算公式:

LOC(j1,j2,....,jn) = LOC(0,0,...,0)+Σni=1 ciji

其中cn=L,cij=bi x ci (1 i就是常数。由于计算各个元素存储位置的时间相等,所以存取数组中任意元素的时间也相等。称有这一特点的存储结构为随机存储结构。其实这样可以看出来,像使用C语言等编程语言的时候,使用数组其实在隐性的进行着下标运算以获得内存位置

typedef struct{
    ElemYype *base;    //数组元素基址
    int dim;    //数组维数
    int *bounds;    //数组维界基址
    int *constants;    //数组映像函数常量基址
}Array;

多为数组的管理方式有点像上一章提到的页指针和行指针。

也许现在再看数组的ADT感觉十分多余,在C语言甚至C++的基础语法学完后感觉数组就没有什么研究的深度可讲。其实不然,如果脱离高级编程语言,使用低级语言比如汇编,这种存储的数据结构还是很有意义的。语言是在发展的,虽然越发展越是对底层的东西不可见,但也正是越底层的东西,越是不经常改变的。

矩阵的压缩存储

在数值分析中经常出现一些阶数很高的矩阵,同时在矩阵中有许多值相同的元素或者是零元素。有时为了节省内存空间,可以对这类矩阵进行压缩存储。所谓的压缩存储是指:为多个值相同的元素分配一个存储空间,对零元素不分配存储空间。

假若值相同的元素或者零元素在矩阵中的分布有一定的规律,则我们称此类矩阵为特殊矩阵,反之,称之为稀疏矩阵

特殊矩阵

若n阶矩阵A中的元素满足下述性质:aij = aji  (1 2个元素压缩到n(n + 1) / 2个元素的空间中,不失一般性,我们可以行序为主序的存储其下三角(包括对角线)中的元。

假设以一维数组sa[n(n +1)/2]作为n阶对称矩阵A的存储结构,则sa[k]和矩阵元aij之间存在着一一对应的关系。

k = i(i - 1)/2 + j - 1 (当i >=j时)

j(j - 1)/2 + i - 1  (当i ij,反之对所有k=0,1,2,....,n(n + 1)/2 - 1中,都能确定sa[k]中的元在矩阵中的位置(i,j)。由此,称sa[n(n +1)/2]作为n阶对称矩阵A的压缩存储。

不仅是对称矩阵,上下三角矩阵也可以如此压缩成一个一维数组。

稀疏矩阵

在实际应用中,我们还经常遇到另一类矩阵,其非零元较零元少,而且分布没有一定规律,我们称之为稀疏矩阵。

稀疏矩阵只是一个凭人们的直觉来了解的概念。假设m X n的矩阵中,有t个元素不为零,令δ=t/(m + n),称δ为矩阵的稀疏因子,通常认为认为δ<= 0.05时称之为稀疏矩阵。

稀疏矩阵的特点是非零元少,那么,这也是压缩策略。

三元顺序表:直接存储行纵坐标及值,当然,为了各种方便,一般以行序或列序为主序顺序排列。

#define MAXSIZE 12500

typedef struct{
    int i,j;        //存储非零元的下标
    ElemType e;
}Triple;
typedef struct{
    Triple data[MAXSIZE + 1];    //非零元三元组表,data[0]未用
    int mu,nu,tu;                //矩阵的行数,列数,和非零元的个数
}TSMatrix;

三元组实现矩阵转置:

1.将矩阵的行列值(mu,nu)互相交换

2.将每个三元组中的i和j相互调换

3.重排三元组之间的次序便可实现矩阵的转置

开始的时候没感觉到什么,可是书中在强调第三步,明明就是一个二级排序嘛,有什么大不了,慢点的话用冒泡,快点用快排,二级排序嘛。

其实不然,如果是两个for进行冒泡,它的时间复杂度是什么?

for(col = 1;col <=M.nu;++col)
    for(p = 1;p <= M.tu;++p)
        if(...)
        {
            ...
        }

注意第二个for,他才是导致这个算法有待考虑的关键,因为这个算法不是单纯的O(长X宽),而是O(长X非零元个数),也就是说,这个算法,严重受到非零元个数影响。

如果不用三元组的方式,不压缩矩阵,时间复杂度明显是O(长X宽)即O(mu X nu),那么如果tu << mu X nu的话,这个算法还是可以考虑的但因为tu的取值范围是[0,mu X nu],万一tu和mu X nu是一个数量级,那么时间复杂度(O(mu X nu^2))就是成倍的增加.

快速转置法提供了这样一种思路,如果已经确定了转换后的存储位置,那么在转置过程中便可以实现重排操作。但是问题来了,要如何确定转置后的位置?动态线性存储和二维数组不一样,我们无法以交换下标的方式来确定位置。

其实快速转置法是这样解决的,它先定义两个向量(说白了就是一维数组)num和cpot,来分别记录每一列中非零元素的个数和第一个非零元转置后在data中适当的位置。其实这个记录方式和数组相仿。数组也不过就是记录了同地址(cpot的作用)和数组长度(num的作用)。在数组中,对头地址加任何一个小于num的数,便可以访问以头地址为行头的这一维元素的任意元素。如果加的数大于num了呢?那就说明这一行完了,开始下一行(维)了。

for(col = 1;col <= M.nu;++col) num[col] = 0;
for(t = 1;t <= M.tu;++t) ++num[M.data[t].j]; //求每一列的非零元个数

//初始化
cpot[1] = 1;
//请第col列中第一个非零元在b.data中的序号
for(col = 2;col <= M.nu;++col) cpot[col] = cpot[col - 1] + num[col - 1];

for(p = 1;p <= M.tu;++p){
    col = M.data[p].j;
    q = cpot[col];
    T.data[q].i = M.data[p].j;
    T.data[q].j = M.data[p].i;
    T.data[q].e = M.data[p].e;
    ++cpot[col];
}

其中++cpot[col];一定要注意,原位置在被占用之后,可使用位置向后推一。

虽然看起来又多了两个for循环,却是并列关系,所以实际上把时间复杂度降到了O(nu + tu)。

三元组顺序表又称有序的双下标法,他的特点是非零元在表中按行序处理的矩阵运算。然而,若需按行号存取某一行的非零元,则需从头开始进行查找。

行逻辑链接的顺序表(三元组的优化)

为了便于随机存取任意一行的非零元,则需知道每一行的第一个非零元在三元组表中的位置。为此,可使用类似上文提到的cpot向量,只不过cpot记录的是各第一个非零元素。我们现在需要的是一个记录各第一个非零元素在data的位置的向量。

那么便重新构建了这一个数据类型:

typedef struct{
    Triple data[MAXSIZE + 1];    //非零元三元组表,data[0]未用
    int rpos[MAXRC + 1];        //各行第一个非零元的位置表
    int mu,nu,tu;                //矩阵的行数,列数,和非零元的个数
}RLSMatrix;

因为可以方便的访问非零元,则这种数据结构可以更方便的进行矩阵乘法

在计算机中,一个矩阵实际上就是一个二维数组。一个m行n列的矩阵与一个n行p列的矩阵可以相乘,得到的结果是一个m行p列的矩阵,其中的第i行第j列位置上的数为第一个矩阵第i行上的n个数与第二个矩阵第j列上的n个数对应相乘后所得的n个乘积之和。(摘自百度百科)

对于二维数组运算只需要:

for(i = 1;i <= m1;++i)
    for(j = 1;j <= n2;++j){
        Q[i][j] = 0;
        for(k = 1;k <= n1;++k) Q[i][j] += M[i][k] * N[k][j];
    }

但对于三元组表,这种算法显然是不合适的。

我们看点本质的东西,如何确定结果Q数组?

Q(i,j) = Σ(n1,k = 1)M(i,k) X N(k,j) (1 <= i <= m1,1 <= j <= n2)

在经典算法中,无论M(i,k)和N(k,j)是否为零,都需要进行一次运算,因此,在对稀疏矩阵运算的时候应该避免这种运算。换句话说,为求Q的值,只需要在M.data和N.data中找到相应的各对元素(即M.data中的j值和N.data中的i值相等的各对元素)相乘即可。

由此可得,如果想要矩阵相乘,需要从N矩阵中找到i值和M矩阵j相同的元素相乘。那么就需要一个确定某行的快速遍历。这样,rpos数组就有用武之地了。

又因为稀疏矩阵的乘积不一定为稀疏矩阵,而且,因为结果矩阵的每个元都是加和而成的,所以,即使相乘不为0,但加和可能为零,也就是说结果三元组可能存在零元,最后需要剔除。

那么算法可以精简为:

1.清零Q(i,j)

2.选取M中的元M(i,j)

3.找到N中i值与M的j值相等的元N(i,j)

4.相乘累加到Q(i,j)

5.压缩Q矩阵

伪代码如下:

Q初始化;
if(Q是非零矩阵){   //逐行求积
    for(arow = 1;arow <= M.mu;++arow){   //处理M的每一行
        ctemp[] = 0  //累加器清零
        计算Q中第arow行的积并存入ctemp[]中;
        将ctemp[]中非零元压缩存储到Q.data;
    }
}

在书中还提供了比较详细的代码,感觉十分美妙,继续整理:

Status MultSMatrix(RLSMatrix M,RLSMatrix N,RLSMatrix &Q)
{
    //求矩阵乘积Q = M X N,采用行逻辑链接表示
    if(M.nu != N.mu) return ERROR;

    //Q初始化
    Q.mu = M.mu;
    Q.nu = N.nu;
    Q.tu = 0;

    if(M.tu * N.tu != 0){  //Q是非零矩阵
        for(arow = 1;arow  MAXSIZE) return ERROR;
                    Qdata[Q.tu] = (arow,ccol,ctemp[ccol]);
                }
        }
    }
    return OK;
}

这个时间复杂度为O(M.mu X N.nu + M.tu X N.tu / N.mu)

一般在快速矩阵乘法中大都采用快速幂,能让时间复杂度压到O(n^2.81)。

3.十字链表

名字很高大上的样子,在集训队总结图的存储结构的时候我自学过一种叫十字链表的东西,它是正邻接表和逆邻接表的结合体,

这里的十字链表是不一样的,我学过的那个结构才真像十字结构,而这里的十字链表有点像网状结构,真想起来当时学链表的时候和王政他们开玩笑说写个链网来代替二维数组,其实这个就是类似实现,只不过里面保存的只是非零元素。

先说说为什么要用十字链表,对于三元组来说,有个问题很奇特,就是当矩阵加法的时候,可能会导致大量零元变成非零元,然后线性表里会增加很多元素,这样来说,顺序便会打乱。如果再次排序的话,时间复杂度不低。与其在末尾添加最后排序不如之间插入到应有的位置(插排),而对于插入,删除等能引起成员位置移动的操作,当然要选链式结构了,时间复杂度能降到一个数量级。

再说说为什么说这像个链网。每个元素是一个节点,保存有5个域(值),其中i,j,e来分别保存该非零元素的行坐标,列坐标和值,向右域right用以链接同一行中下一个非零元,向下域down用以链接同一列中下一个非零元。同一行的非零元通过right域链接成一条线性表,同一列非零元素通过down域链接成一条线性表。然后在矩阵中有chead和ehead数组来控制坐标头地址,与其说形成一个十字结构不如说是一个网状结构(但是只是考虑单一结点,还是是个十字的)。

typedef struct OLNode{
    int i,j;
    ElemType e;
    struct OLNode *right,*down;
}OLNode;

typedef struct{
    Olink *rhead,*chead;
    int mu,nu,tu;
}CrossList;

对于十字链表的使用,比链表也难不到哪去,一定要有模型的概念,注意细节就好了。

最后说一下对于十字链表处理矩阵加法。

其实不难,就是繁琐,按行或列处理,一个一个相加,注意一共四种情况:

1.A为不为零,B不为零,加和不为零

2.A为零,B不为零

3.A不为零,B为零

4.1.A为不为零,B不为零,加和为零

处理好这四种情况的对链结的增减,矩阵加法也不过如此。


虽然矩阵在这一章属于数组范围,但是矩阵运算着实不简单,之所以不简单因为离不开线性表的灵活运用。

2014/10/11 15:41 pm posted in  算法&数据结构

C++ 循环和关系表达式

for循环的步骤:

  • 设置初始值
  • 执行测试,看看循环是否应当继续进行
  • 执行循环操作
  • 更新用于测试的值

在C++中,常在for和while等控制语句中,如for,在for和括号之间增写一个空格,以强化控制语句与函数调用之间的区别.。

前缀自增减会将值加1减1,然后返回结果,而后缀自增减首先要创建一个副本,将值加1减1,然后将复制的副本返回,因此,对于类来说,前缀版本的效率要比后缀版本的效率高。

对于内置类型,采用前后缀不会有差别,但对于用户定义的类型,前缀格式的效率更高。

自增减的优先级与*相同,但如果同时出现会从右到左执行(注意指针上的应用)。

string不使用空字符来标记字符串的末尾。

在头文件ctime中,提供了clock()函数来调用系统时间,和循环配合使用可以有效的控制和实现相关程序停顿等问题。

ctime中还定义了CLOCKS_PER_SEC来标记系统的时间与秒的转换参数。

cin >> secs;
clock_t delay = secs * CLOCKS_PER_SEC;
clock_t start = clock();
while(clock() - start

C++11新增了一种基于范围的for循环,简化了一种常见的任务:对数组(或容器类)的每个元素执行相同的操作。

double num[5] = {1.0,5.2,5.4,5.1,0.2};

for(double x : num)
cout

很C++11总是给人高大上的感觉。

但是要注意的是,如果需要修改x的值,则需要添加上引用。

如果和各种容器配合,这种用法将更加方便,类似:

for(int x : {1,2,3,4,5,6})
cout

用cin输入的时候,文件结尾依然是EOF,但是cin是如何判断EOF的呢?

之前用C做题,写的是while(scanf(...) != EOF)

而C++写的是while(cin >> x)

看起来没有任何和EOF有关的判断,但是cin当检测到EOF时是有标记的的

在第四章写过失效位(failbit),其实这里和这个类似,用的是eofbit,如果cin检测到EOF,则,cin.eof()将返回bool值true,否则返回false。

其实无论是failbit和eofbit,只要有一个被标记为1,则cin.fail()则为true,然后cin便失效,不再工作,需要cin.clear()清除标记。

C++也有怀旧情结,因此广泛支持C语言,甚至一些写法故意的怀旧,比如为了模仿getchar和putchar

cin和cout可以:

ch = cin.get();
cout.put(ch);

当然,很明显,这是函数重载实现的(《C++ PP》讲的是这么的慢,小笨球总结的谭浩强那本都讲完重载了,这个还在讲cin)。

2014/10/09 01:00 am posted in  C++