1、2/8/2019,1,Data Structures, Algorithms, and Applications in C+数据结构、算法与应用-C+描述 Sartaj Sahni著孙明 S,2/8/2019,2,数据结构算法与应用-C+语言描述机械工业出版社 2002.10 535页 49元C+程序设计语言(特别版)美:Bjarne Stroustrup 译:裘宗燕 机械工业出版社 2002.7 936页URL:www.cise.ufl.edu/sahni/dsac,参考书籍,2/8/2019,3,教学日历,2/8/2019,4,教学日历,2/8/2019,5,教学日历,2/8/2019,6
2、,教学日历,2/8/2019,7,教学日历,2/8/2019,8,计算机科学的重要基础课程 什么是数据结构 数据结构发展历史 C+实践,引言,2/8/2019,9,软件设计是一种智力的挑战。程序设计技术是一种组织复杂性的技术,是一种控制巨量数据的技术,也是一种尽量回避混乱的技术。 高效描述数据;设计好的算法 例:查字典,图书馆借书,搜索引擎 Googol表示“10的100次方,巨大的数字”。当年google公司的创始人选择了googol的同音异形体google作为公司的大名,意在表现该引擎“搜集和驾御浩瀚无穷的网络信息”的宏图。,计算机科学的重要基础课程,2/8/2019,10,数据是描述客观
3、事物用到的数、字符以及所有能输入到计算机中并能被计算机程序处理的符号集合,它是计算机程序使用、加工的原料和输出的结果。 算法是一个有穷规则的集合,规定了解决某一特定问题的运算序列(输入、输出、确定性、有穷性、能行性)。,什么是数据结构,2/8/2019,11,1.数据元素间的逻辑结构关系(面向应用)线性非线性-层次、网状 2.数据的存贮结构(面向存储)内存-顺序,链式,散列,索引物理 3.对数据进行的运算及实现方法(查找、插入、删除),什么是数据结构,2/8/2019,12,线性结构,谭维维,艾梦萌,刘力扬,厉娜,REBORN,1 2 3 4 5,元素之间为一对一的线性关系,第一个元素无直接前
4、驱,最后一个元素无直接后继,其余元素都有一个直接前驱和直接后继。,2/8/2019,13,非线性结构,元素之间为一对多非线性关系的非线性结构称为树结构,除根结点无直接前驱、有多个直接后继外,其余元素均有一个直接前驱或多个直接后继。 或元素之间为多对多非线性关系的非线性结构称为图结构,每个元素均有多个直接前驱或多个直接后继。,2/8/2019,14,树形结构,14,13,12,11,2,3,4,5,6,7,8,9,10,3,1,5,8,7,10,11,9,9,8,7,4,5,6,6,2,3,13,1,1,树 二叉树 二叉搜索树,2/8/2019,15,群聚类,图结构 网络结构,1,2,5,6,4
5、,3,1,2,5,4,3,6,11,33,18,14,6,6,5,16,19,21,2/8/2019,16,顺序存贮(向量存贮) : 依次、连续所有元素存放在一片连续的存贮单元中,逻辑上相邻的元素存放到计算机内存仍然相邻。,存贮结构-顺序存贮,2/8/2019,17,存贮结构-顺序存贮,设有数据元素a1、 a2 、 、an ,顺序存贮形为:,2/8/2019,18,链式存贮 :依次、未必连续所有元素依次存放在可以不连续的存贮单元中,元素之间的关系可以通过指针(地址)表示。,存贮结构-链式存贮,a0,a1,a2,a3,a4,first,2/8/2019,19,使用附加的索引表,索引表中的每一项称
6、为索引项,其一般形式是:(关键字,地址),其中的关键字是能唯一标识一个结点的那些数据项。,存贮结构-索引存贮,存贮结构-索引存贮,2/8/2019,21,通过构造散列函数,用函数的值来确定元素存放的地址。即:元素ai 的地址=Hash(ai),存贮结构-散列存贮,存贮结构-散列存贮,2/8/2019,23,定义:一个有穷的指令集,这些指令为解决某一特定任务规定了一个运算序列。 特性:输入 有0个或多个输入输出 有一个或多个输出(处理结果)确定性 每步定义都是确切、无歧义的有穷性 每条指令的执行次数必须是有限的有效性 每条指令的执行时间都是有限的,算法定义,2/8/2019,24,运算的定义直接
7、依赖于逻辑结构。 运算的实现依赖于存贮结构。 例:顺序查找、折半查找,算法和数据结构密切相关,2/8/2019,25,1969年,美国科学家Donald E.Knuth 出版巨著计算机程序设计艺术第一卷基本算法,全面、系统讨论了各种数据结构,定义了其上的运算和算法。 数据结构与算法的奠基人。 “世界历史上最伟大的十种学科著作”之一。 计划7卷,1974年出3卷后获图灵奖(36岁),数据结构发展历史,2/8/2019,26,注重实践 正确、易读、易维护、性能、通用,C+实践,2/8/2019,27,Functions and Parameters Dynamic Memory Allocatio
8、n Classes Testing and Debugging,Chapter 1 Programming in C+ (第1章 C+程序设计),2/8/2019,28,递归,本章重点,2/8/2019,29,它正确吗? 它容易读懂吗? 它有完善的文档吗? 它容易修改吗? 它在运行时需要多大内存? 它的运行时间有多长? 它的通用性如何?能不能不加修改就可以用它来解决更大范围的问题? 它可以在多种机器上编译和运行吗?或者说需要经过修改才能在不同的机器上运行吗?,编程要素,2/8/2019,30,Value Parameters Template Functions Reference Param
9、eters Const Reference Parameters Return Values 递归函数(Recursive Functions) 斐波那契数列(Fibonacci numbers) 阶乘(Factorial) 排列(Permutations),1.1 Functions and Parameters,2/8/2019,31,程序1-1 计算一个整数表达式 int Abc(int a, int b, int c) return a+b+b*c+(a+b-c)/(a+b)+4; z=Abc(2,x,y) 形式参数( formal parameter) 实际参数(actual par
10、ameter) 复制构造函数( copy constructor) 析构函数(destructor),传值参数(Value Parameters),2/8/2019,32,程序1-2 计算一个浮点数表达式 float Abc(float a, float b, float c) return a+b+b*c+(a+b-c)/(a+b)+4; ,模板函数(Template Functions),2/8/2019,33,实际上不必对每一种可能的形式参数的类型都重新编写一个相应的函数。可以编写一段通用的代码,将参数的数据类型作为一个变量,它的值由编译器来确定。 template T Abc(T a,
11、 T b, T c) return a+b+b*c+(a+b-c)/(a+b)+4; ,模板函数(Template Functions),2/8/2019,34,传值参数形式参数的用法会增加程序的运行开销。 程序1-3中,在函数被调用时,类型T 的复制构造函数把相应的实际参数分别复制到形式参数a,b 和c 之中,以供函数使用;而在函数返回时,类型T的析构函数会被唤醒,以便释放形式参数a,b 和c。,传值参数?,2/8/2019,35,程序1-4 利用引用参数计算一个表达式 template T Abc(T ,引用参数(Reference Parameters),2/8/2019,36,如果用语
12、句Abc(x,y,z)来调用函数Abc,其中x、y和z是相同的数据类型,那么这些实际参数将被分别赋予名称a,b和c。 因此,在函数Abc执行期间,x、y和z被用来替换对应的a,b和c。 与传值参数的情况不同,在函数被调用时,本程序并没有复制实际参数的值,在函数返回时也没有调用析构函数。,引用参数(Reference Parameters),2/8/2019,37,Java 应用程序有且仅有的一种参数传递机制,即按值传递。 基本类型作为参数传递时,是按值传递。 在Java中对象作为参数传递时,是把对象在内存中的地址拷贝了一份传给了参数,传递的是对象引用的值。,Java中的参数传递,2/8/201
13、9,38,template T Abc(const T 这种模式指出函数不得修改引用参数。,常量引用参数(Const Reference Parameters),2/8/2019,39,使用关键字const 来指明函数不可以修改引用参数的值,这在软件工程方面具有重要的意义。这将立即告诉用户该函数并不会修改实际参数。 编程建议: 对于诸如i n t、float 和char 的简单数据类型,当函数不会修改实际参数值的时候我们可以采用传值参数;对于所有其他的数据类型(包括模板类型),当函数不会修改实际参数值的时候可以采用常量引用参数。,常量引用参数(Const Reference Parameter
14、s),2/8/2019,40,函数可以返回值,引用或常量引用。 在前面的例子中,函数Abc 返回的都是一个具体值,在这种情况下,被返回的对象均被复制到调用(或返回)环境中。 如果需要返回一个引用,可以为返回类型添加一个前缀&。如:T& X(int i, T& z),返回值(Return Values),2/8/2019,41,递归:一个事物部分地由它自身构成或由自身来定义。 递归调用是解决某类特殊问题的好方法。但在现实生活中很难找到类似的比照。有一个广为流传的故事,倒是可以看出点“递归”的样子。“从前有座山,山里有座庙,庙里有个老和尚,老和尚对小和尚说故事:从前有座山”。在讲述故事的过程中,又
15、嵌套讲述了故事本身。,递归,2/8/2019,42,递归函数是一个自己调用自己的函数。 递归函数包括两种:直接递归(direct recursion)和间接递归(indirect recursion)。直接递归是指函数F的代码中直接包含了调用F的语句,而间接递归是指函数F调用了函数G,G又调用了H,如此进行下去,直到F又被调用。,递归函数(Recursive Functions),2/8/2019,43,在数学中经常用一个函数本身来定义该函数。例如阶乘函数令f(n)=n!:,数学函数的递归定义,2/8/2019,44,对于函数f(n)的一个递归定义(假定是直接递归),要想使它成为一个完整的定义
16、,必须满足如下条件: 定义中必须包含一个基本部分(base),其中对于n的一个或多个值,f(n)必须是直接定义的(即非递归)。为简单起见,我们假定基本部分包含了nk的情况,其中k为常数。 在递归部分(recursive component)中,右侧所出现的所有f的参数都必须有一个比n小,以便重复运用递归部分来改变右侧出现的f,直至出现f的基本部分。,数学函数的递归定义,2/8/2019,45,在阶乘函数公式中, 基本部分是:当n1时f(n)=1; 递归部分是f(n)=nf(n-1),其中右侧f的参数为n-1,比n要小。 重复应用递归部分可把f(n-1)变换成对f(n-2),f(n-3),. ,
17、直到f(1)的调用。 例如: f(5)=5f(4)=20f(3)=60f(2)=120f(1) 每次应用递归部分的结果是更趋近于基本部分,最后,根据基本部分的定义可以得到f(5)=120。,数学函数的递归定义,2/8/2019,46,斐波那契数列的定义:F0=0,F1=1,Fn=Fn-1+Fn-2 (n1)基本部分: ? 递归部分: ?,数学函数的递归定义,2/8/2019,47,证明方法可以归纳为三个部分 归纳初值(induction base) 归纳假设(induction hypothesis) 归纳步证明(induction step),归纳证明,2/8/2019,48,像递归定义并不
18、是循环定义一样,归纳证明并不是循环证明。 每个正确的归纳证明都会有一个基本值验证部分,它与递归定义的基本部分相类似,在归纳证明时我们利用了比n值小时结论的正确性来证明取值为n时结论的正确性。重复应用归纳证明,可以减少对基本值验证的应用。,归纳证明,2/8/2019,49,C+允许编写递归函数。 一个正确的递归函数必须包含一个基本部分。函数中递归调用部分所使用的参数值应比函数的参数值要小,以便函数的重复调用能最终获得基本部分所提供的值。,C+中的递归函数,2/8/2019,50,程序1-7计算n!的递归函数 int Factorial(int n) /计算n!if(n=1) return 1;r
19、eturn n*Factorial(n-1); ,阶乘(Factorial),2/8/2019,51,递归调用过程中:程序的状态(如局部变量、传值形式参数的值、引用形式参数的值以及代码的执行位置等)被保留在递归栈中。,递归调用过程,2/8/2019,52,递归过程,递归过程在实现时,需要自己调用自己。 层层向下递归,退出时的次序正好相反:递归调用n! (n-1)! (n-2)! 2! 1!=1 返回次序,2/8/2019,53,递归工作栈,每一次递归调用,需要分配存储空间,来保留程序的状态(如局部变量、参数值、代码的执行位置等) 。 每层递归调用需分配的空间形成递归工作记录,按后进先出的栈组织
20、。,局部变量 返回地址 参 数,活动记录框架,递归 工作记录,2/8/2019,54,递归优点,递归简洁、易编写、易懂 易证明正确性,2/8/2019,55,递归改为非递归,递归效率低,重复计算多 改为非递归的目的是提高效率 单向递归可直接用迭代实现非递归 其他情形必须借助栈实现非递归,2/8/2019,56,计算n!的非递归函数 int Factorial(int n) /计算n!if(n=1) return 1;int f=2;for(int i=3;i=n;i+)f*=i;return f; ,阶乘非递归实现,2/8/2019,57,斐波那契数列非递归实现,long Fib ( long
21、 n ) if ( n = 1 ) return n;long twoback = 0, oneback = 1, Current;for ( int i = 2; i = n; i+ ) Current = twoback + oneback;twoback = oneback; oneback = Current;return Current; ,2/8/2019,58,template T Sum(T a,int n) /计算a0:n-1的和T tsum = 0;for(int i=0;i0时,n个元素的和是前面n-1个元素的和加上最后一个元素。,程序1-8累加a0:n-1,2/8/20
22、19,59,template TRsum(T a,int n) /计算a0:n-1的和if(n0)return Rsum(a,n-1)+an-1;return 0; ,程序1-9递归计算a0:n-1,2/8/2019,60,上楼梯,递归思想,2/8/2019,61,a,b和c的排列方式有:abc,acb,bac,bca,cab和cba。 n个元素的排列方式共有n!种。 采用非递归的C+函数来输出n个元素的所有排列方式很困难。,排列(Permutations),2/8/2019,62,令E=e1,.,en表示n个元素的集合,令Ei为E中移去元素i以后所获得的集合,perm(X)表示集合X中元素的
23、排列方式,ei.perm(X)表示在perm(X)中的每个排列方式的前面均加上ei以后所得到的排列方式。 例如,如果E=a,b,c,那么E1=b,c,perm(E1)=(bc,cb),e1.perm(E1)=(abc,acb)。,定义,2/8/2019,63,对于递归的基本部分,采用n=1。当只有一个元素时,只可能产生一种排列方式,所以perm(E)=(e),其中e是E中的唯一元素。 递归部分:当n1时,perm(E)=e1.perm(E1) +e2.perm(E2)+e3.perm(E3)+en.perm(En)。这种递归定义形式是采用n个perm(X)来定义perm(E),其中每个X包含n
24、-1个元素。,排列递归思路,2/8/2019,64,当n=3并且E=(a,b,c)时,按照前面的递归定义可得perm(E)=a.perm(b,c)+b.perm(a,c)+c.perm(a,b)。 同样,按照递归定义有perm(b,c)=b.perm(c)+c.perm(b),所以a.perm(b,c)=ab.perm(c)+ac.perm(b)=ab.c+ac.b=(abc,acb)。 同理可得b.perm(a,c)=ba.perm(c)+bc.perm(a)=ba.c+bc.a=(bac,bca),c.perm(a,b)=ca.perm(b)+cb.perm(a)=ca.b+cb.a=(c
25、ab,cba)。 所以perm(E)=(abc,acb,bac,bca,cab,cba)。,排列递归模拟,2/8/2019,65,程序1-10使用递归函数生成排列 template Void Perm(T list,int k,int m) /生成listk:m的所有排列方式int i;if(k = m)/输出一个排列方式for(i=0;i=m;i+)coutlisti;coutendl;else/listk:m有多个排列方式,递归地产生这些排列方式for(i=k;i=m;i+)Swap(listk,listi);Perm(list,k+1,m);Swap(listk,listi); ,排列递
26、归实现,2/8/2019,66,程序1-11交换两个值 template inline void Swap(T ,Swap函数,2/8/2019,67,The Operator new One-Dimensional Arrays Exception Handling The Operator delete Two-Dimensional Arrays,1.2 Dynamic Memory Allocation,2/8/2019,68,C/C+定义了4个内存区间: 代码区 全局变量与静态变量区 局部变量区(栈区) 动态存储区,即堆(heap)区或自由存储区(free store),C+内存分配
27、,2/8/2019,69,通常定义变量(或对象),编译器在编译时都可以根据该变量(或对象)的类型知道所需内存空间的大小,从而系统在适当的时候为它们分配确定的存储空间。这种内存分配称为静态存储分配; 有些操作对象只在程序运行时才能确定,这样编译时就无法为它们预定存储空间,只能在程序运行时,系统根据运行时的要求进行内存分配,这种方法称为动态存储分配。 所有动态存储分配都在堆区中进行。,堆的概念,2/8/2019,70,当程序运行到需要一个动态分配的变量或对象时,必须向系统申请取得堆中的一块所需大小的存贮空间,用于存贮该变量或对象。 当不再使用该变量或对象时,也就是它的生命结束时,要显式释放它所占用
28、的存贮空间,这样系统就能对该堆空间进行再次分配,做到重复使用有限的资源。,堆的概念,2/8/2019,71,C+操作符new可用来进行动态存储分配,该操作符返回一个指向所分配空间的指针。 int *y ; y = new int; *y = 10; 操作符new分配了一块能存储一个整数的空间,并将指向该空间的指针返回给y,y是对整数指针的引用,而*y则是对整数本身的引用。,The Operator new,2/8/2019,72,动态存储分配方法: 为了在运行时创建一个一维浮点数组x,首先必须把x说明成一个指向float的指针,然后为数组分配足够的空间。 例如,一个大小为n的一维浮点数组可以按
29、如下方式来创建:float *x=new floatn; 操作符new分配n个浮点数所需要的空间,并返回指向第一个浮点数的指针。可以使用如下语法来访问每个数组元素:x0,x1,.,xn-1。,One-Dimensional Arrays,2/8/2019,73,Javafloat x=new floatn;,One-Dimensional Arrays,2/8/2019,74,动态分配的存储空间不再需要时应该被释放,所释放的空间可重新用来动态创建新的结构。 可以使用C+ 操作符delete来释放由操作符new所分配的空间。下面的语句可以释放分配给* y的空间以及一维数组x: delete y;
30、 delete x;,The Operator delete,2/8/2019,75,可以看成是由若干行组合起来的,每一行都是一个一维数组。,Two-Dimensional Arrays,2/8/2019,76,x0, x1, x2分别指向第0行,第1行和第2行的第一个元素。如果x是一个字符数组,那么x 0:2是指向字符的指针,而x本身是一个指向指针的指针。可用如下语法来说明x :char *x;,Two-Dimensional Arrays,2/8/2019,77,template void Make2DArray( T * ,程序1-13 创建一个二维数组,2/8/2019,78,temp
31、late void Delete2DArray(T * ,程序1-14 释放由Make2DArray所分配的空间,2/8/2019,79,The Class Currency(ADT,封装性) Operator Overloading(可读性) Throwing Exceptions,1.3 Classes,2/8/2019,80,class Currency public:/ 构造函数 Currency(sign s=plus,unsigned long d=0, unsigned int c=0);/ 析构函数Currency() bool Set(sign s, unsigned lon
32、g d, unsigned int c);bool Set(float a);sign Sign() const return sgn;unsigned long Dollars() const return dollars;unsigned int Cents() const return cents;Currency Add(const Currency,程序1-15 定义Currency类,2/8/2019,81,程序1-16 Currency类的构造函数 Currency:Currency(sign s, unsigned long d, unsigned int c) / 创建一个C
33、urrency对象if(c 99) /美分数目过多cerr “Cents should be100“endl;exit (1) ; sgn = s; dollars = d; cents = c; ,类的方法实现,2/8/2019,82,程序1-19 Increment Currency ,类的方法实现,2/8/2019,83,#include #include “curr1.h“ void main (void) Currency g, h(plus, 3, 50), i, j;g.Set(minus, 2, 25);i . Set( -6.4 5 ) ;j = h.Add(g);j.Out
34、put(); cout endl;i . Increment( h ) ;i.Output(); cout endl;j = i.Add(g).Add(h);j.Output(); cout endl;j = i.Increment(g).Add(h);j.Output(); cout endl;i.Output(); cout endl; ,程序1-20 Currency类应用示例,2/8/2019,84,程序1-25 + , 的代码 Currency Currency:operator+(const Currency,Operator Overloading,2/8/2019,85,程序1
35、-26 操作符重载的应用 #include #include “curr3.h“ void main(void) Currency g, h(plus, 3, 50), i, j;g.Set(minus, 2, 25);i . Set(-6 . 4 5 ) ;j = h + g;cout j endl;i += h;cout i endl;j = i + g + h;cout j endl; ,Operator Overloading,2/8/2019,86,软件工程实践告诉我们,数据成员应尽量保持为private成员。 通过增加保护类成员来访问和修改数据成员的值,派生类可以间接访问基类的数据
36、成员。同时,可以修改基类的实现细节而不会影响派生类。,建议,2/8/2019,87,What Is Testing? (数学证明困难) Designing Test Data Debugging,1.4 Testing and Debugging,2/8/2019,88,由于采用严格的数学证明方法来证明一个程序的正确性是非常困难的,所以转而求助于程序测试(program test)过程来实施这项工作。 所谓程序测试是指在目标计算机上利用输入数据,也称之为测试数据( test data)来实际运行该程序,把程序的实际行为与所期望的行为进行比较。如果两种行为不同,就可判定程序中有问题存在。 然而,
37、不幸的是,即使两种行为相同,也不能够断定程序就是正确的,因为对于其他的测试数据,两种行为又可能不一样。,What Is Testing?,2/8/2019,89,如果使用了许多组测试数据都能够看到这两种行为是一样的,我们可以增加对程序正确性的信心。通过使用所用可能的测试数据,可以验证一个程序是否正确。 然而,对于大多数实际的程序,可能的测试数据的数量太大了,不可能进行穷尽测试,实际用来测试的输入数据空间的子集称之为测试集(test set)。,What Is Testing?,2/8/2019,90,测试的目的不是去建立正确性认证,而是为了发现尽可能多的缺陷(功能错误/性能低下/易用性差) 一
38、个成功的测试示例在于发现了至今尚未发现的缺陷。,测试的目的,2/8/2019,91,二次方程求解 一个关于变量x 的二次函数形式如下:ax2 + bx +c对于该程序来说,所有可能的输入数据的数目实际上就是所有不同的三元组(a,b,c)的数目,其中a0。,测试用例,2/8/2019,92,即使a, b和c 都被限制为整数,所有可能的三元组的数目也是非常巨大。 若整数的长度为16位,b 和c 都有216 种不同取值,a 有216-1种不同取值(因为a 不能为0),所有不同三元组的数目将达到232(216-1)。 如果目标计算机能按一百万个/每秒钟 三元组的速率进行测试,那么至少需要9年才能完成!
39、如果使用一个更快的计算机,按十亿个/每秒三元组的速度,也至少需要三天才能完成。 所以一个实际使用的测试集仅是整个测试数据空间中的一个子集。,测试用例,2/8/2019,93,测试数据选择条件: 这个数据能够发现错误的潜力如何? 能否验证采用这个数据时程序的正确性? 黑盒法(black box method) 等价类/边界值 测试 白盒法(white box method) 基于对代码的考察来设计测试数据。 测试 ;,设计测试数据,2/8/2019,94,语句覆盖(statement coverage) 分支覆盖(decision coverage) 从句覆盖(clause coverage)
40、路径覆盖(execution path coverage),白盒法(white box method),2/8/2019,95,语句: if(C1 其中C1,C2,C3和C4是从句,S1和S2是语句。 从句覆盖要求测试数据能使四个从句C1,C2,C3和C 4都分别至少取一次true值和至少取一次false值。,从句覆盖,2/8/2019,96,template int Max(T a, int n) / 寻找a 0 : n - 1 中的最大元素int pos = 0;for (int i = 1; i n; i+)if (apos ai)pos = i;return pos; 数据集a0:4=
41、2,4,6,8,9提供何种覆盖 ? 数据集a0:4=4,2,6,8,9提供何种覆盖 ?,程序1-31 寻找最大元素,2/8/2019,97,Edsger Wybe Dijkstra(Algol60编译器设计者和实现者,THE操作系统的设计者,72年图灵奖): “编程的艺术是处理复杂性的技术”。 “测试只能证明错误的存在,不能证明错误不存在”。,关于测试,2/8/2019,98,测试能够发现程序中的错误。一旦测试过程中产生的结果与所期望的结果不同,就可以了解到程序中存在错误。 确定并纠正程序错误的过程被称为调试(debug)。 逻辑推理 程序跟踪,调试(debug),2/8/2019,99,参数传递 递归思想 测试调试,第一章总结,2/8/2019,100,a.体会,学习计划。 b.5.试编写一个递归函数,用来输出n个元素的所有子集。例如,三个元素a,b,c的所有子集是:(空集),a,b,c,a,b,a,c,b,c和a,b,c。,作业,