资源简介 (共13张PPT)一. 问题的提出1.待排序的记录数量很大,不能一次装入内存,则无法利用前几节讨论的排序方法 (否则将引起频繁访问内存);2.对外存中数据的读/写是以“数据块”为单位进行的;读/写外存中一个“数据块”的数据所需要的时间为:TI/O = tseek + tla + n twm其中tseek 为寻查时间(查找该数据块所在磁道)tla 为等待(延迟)时间n twm 为传输数据块中n个记录的时间1.按可用内存大小,利用内部排序 方法,构造若干( 记录的) 有序子序列,通常称外存中这些记录有序子序列为 “归并段”;二、外部排序的基本过程由相对独立的两个步骤组成:2.通过“归并”,逐步扩大 (记录的)有序子序列的长度,直至外存中整个记录序列按关键字有序为止。例如:假设有一个含10,000个记录的磁盘文件,而当前所用的计算机一次只能对1,000个记录进行内部排序,则首先利用内部排序的方法得到10个初始归并段,然后进行逐趟归并。假设进行2 路归并(即两两归并),则第一趟由10个归并段得到5个归并段;最后一趟归并得到整个记录的有序序列。第三趟由 3 个归并段得到2个归并段;第二趟由 5 个归并段得到3个归并段;假设“数据块”的大小为200,即每一次访问外存可以读/写200个记录。则对于10,000个记录,处理一遍需访问外存100次(读和写各50次)。分析上述外排过程中访问外存(对外存进行读/写)的次数:由此,对上述例子而言,1)求得10个初始归并段需访问外存100次;2)每进行一趟归并需访问外存100次;3)总计访问外存 100 + 4 100 = 500次外排总的时间还应包括内部排序所需时间和逐趟归并时进行内部归并的时间,显然,除去内部排序的因素外,外部排序的时间取决于逐趟归并所需进行的“趟数”。例如,若对上述例子采用5 路归并,则只需进行2趟归并,总的访问外存的次数将压缩到 100 + 2 100 = 300 次一般情况下,假设待排记录序列含 m 个初始归并段,外排时采用 k 路归并,则归并趟数为 logkm ,显然,随之k的增大归并的趟数将减少,因此对外排而言,通常采用多路归并。k 的大小可选,但需综合考虑各种因素。1. 了解排序的定义和各种排序方法的特点。熟悉各种方法的排序过程及其依据的原则。基于“关键字间的比较”进行排序的方法可以按排序过程所依据的不同原则分为插入排序、交换排序、选择排序、归并排序和计数排序等五类。2. 掌握各种排序方法的时间复杂度的分析方法。能从“关键字间的比较次数”分析排序算法的平均情况和最坏情况的时间性能。按平均时间复杂度划分,内部排序可分为三类:O(n2)的简单排序方法,O(nlogn)的高效排序方法 和 O(dn)的基数排序方法。3.理解排序方法“稳定”或“不稳定”的含义,弄清楚在什么情况下要求应用的排序方法必须是稳定的。4. 了解外部排序的基本过程及其时间分析。(共174张PPT)6.1 树的类型定义6.2 二叉树的类型定义6.3 二叉树的存储结构6.4 二叉树的遍历6.5 线索二叉树6.6 树和森林的表示方法6.7 树和森林的遍历6.8 哈夫曼树与哈夫曼编码6.1树的类型定义数据对象 D:D是具有相同特性的数据元素的集合。若D为空集,则称为空树;否则:(1) 在D中存在唯一的称为根的数据元素root,(2) 当n>1时,其余结点可分为m (m>0)个互不相交的有限集T1, T2, …, Tm, 其中每一棵子集本身又是一棵符合本定义的树,称为根root的子树。数据关系 R:基本操作:查 找 类插 入 类删 除 类Root(T) // 求树的根结点查找类:Value(T, cur_e) // 求当前结点的元素值Parent(T, cur_e) // 求当前结点的双亲结点LeftChild(T, cur_e) // 求当前结点的最左孩子RightSibling(T, cur_e) // 求当前结点的右兄弟TreeEmpty(T) // 判定树是否为空树TreeDepth(T) // 求树的深度TraverseTree( T, Visit() ) // 遍历InitTree(&T) // 初始化置空树插入类:CreateTree(&T, definition)// 按定义构造树Assign(T, cur_e, value)// 给当前结点赋值InsertChild(&T, &p, i, c)// 将以c为根的树插入为结点p的第i棵子树ClearTree(&T) // 将树清空删除类:DestroyTree(&T) // 销毁树的结构DeleteChild(&T, &p, i)// 删除结点p的第i棵子树ABCDEFGHIJMKLA( )T1T3T2树根例如:B(E, F(K, L)),C(G),D(H, I, J(M))(1) 有确定的根;(2) 树根和子树根之间为有向关系。有向树:有序树:子树之间存在确定的次序关系。无序树:子树之间不存在确定的次序关系。基 本 术 语结点:结点的度:树的度:叶子结点:分支结点:数据元素+若干指向子树的分支分支的个数树中所有结点的度的最大值度为零的结点度大于零的结点DHIJM(从根到结点的)路径:孩子结点、双亲结点、兄弟结点、堂兄弟祖先结点、子孙结点结点的层次:树的深度:由从根到该结点所经分支和结点构成ABCDEFGHIJMKL假设根结点的层次为1,第l 层的结点的子树根结点的层次为l+1树中叶子结点所在的最大层次任何一棵非空树是一个二元组Tree = (root,F)其中:root 被称为根结点,F 被称为子树森林森林:是m(m≥0)棵互不相交的树的集合ArootBEFKLCGDHIJMF对比树型结构和线性结构的结构特点线性结构树型结构第一个数据元素(无前驱)根结点(无前驱)最后一个数据元素(无后继)多个叶子结点(无后继)其它数据元素(一个前驱、一个后继)其它数据元素(一个前驱、多个后继)6.2二叉树的类型定义二叉树或为空树;或是由一个根结点加上两棵分别称为左子树和右子树的、互不相交的二叉树组成。ABCDEFGHK根结点左子树右子树二叉树的五种基本形态:N空树只含根结点NNNLRR右子树为空树L左子树为空树左右子树均不为空树二叉树的主要基本操作:查 找 类插 入 类删 除 类Root(T); Value(T, e); Parent(T, e);LeftChild(T, e); RightChild(T, e);LeftSibling(T, e); RightSibling(T, e);BiTreeEmpty(T); BiTreeDepth(T);PreOrderTraverse(T, Visit());InOrderTraverse(T, Visit());PostOrderTraverse(T, Visit());LevelOrderTraverse(T, Visit());InitBiTree(&T);Assign(T, &e, value);CreateBiTree(&T, definition);InsertChild(T, p, LR, c);ClearBiTree(&T);DestroyBiTree(&T);DeleteChild(T, p, LR);二叉树 的重要特性性质 1 : 在二叉树的第 i 层上至多有2i-1个结点。 (i≥1)用归纳法证明:归纳基:归纳假设:归纳证明:i = 1 层时,只有一个根结点,2i-1 = 20 = 1;假设对所有的 j,1≤ j i,命题成立;二叉树上每个结点至多有两棵子树,则第 i 层的结点数 = 2i-2 2 = 2i-1 。性质 2 : 深度为 k 的二叉树上至多含 2k-1 个结点(k≥1)证明:基于上一条性质,深度为 k 的二叉树上的结点数至多为20+21+ +2k-1 = 2k-1性质 3 : 对任何一棵二叉树,若它含有n0 个叶子结点、n2 个度为 2 的结点,则必存在关系式:n0 = n2+1证明:设 二叉树上结点总数 n = n0 + n1 + n2又 二叉树上分支总数 b = n1 + 2n2而 b = n-1 = n0 + n1 + n2 - 1由此, n0 = n2 + 1两类特殊的二叉树:满二叉树:指的是深度为k且含有2k-1个结点的二叉树。完全二叉树:树中所含的 n 个结点和满二叉树中编号为 1 至 n 的结点一一对应。123456789101112131415abcdefghij性质 4 : 具有 n 个结点的完全二叉树的深度为 log2n +1证明:设 完全二叉树的深度为 k则根据第二条性质得 2k-1≤ n < 2k即 k-1 ≤ log2 n < k因为 k 只能是整数,因此, k = log2n + 1性质 5 :若对含 n 个结点的完全二叉树从上到下且从左至右进行 1 至 n 的编号,则对完全二叉树中任意一个编号为 i 的结点: (1) 若 i=1,则该结点是二叉树的根,无双亲, 否则,编号为 i/2 的结点为其双亲结点; (2) 若 2i>n,则该结点无左孩子, 否则,编号为 2i 的结点为其左孩子结点; (3) 若 2i+1>n,则该结点无右孩子结点, 否则,编号为2i+1 的结点为其右孩子结点。6.3二叉树的存储结构二、二叉树的链式存储表示一、 二叉树的顺序存储表示#define MAX_TREE_SIZE 100// 二叉树的最大结点数typedef TElemTypeSqBiTree[MAX_TREE_SIZE];// 0号单元存储根结点SqBiTree bt;一、 二叉树的顺序存储表示例如:ABCDEFA B D C E F0 1 2 3 4 5 6 7 8 9 10 11 12 131401326二、二叉树的链式存储表示1. 二叉链表2.三叉链表3.双亲链表4.线索链表ADEBCF rootlchild data rchild结点结构:1. 二叉链表typedef struct BiTNode { // 结点结构TElemType data;struct BiTNode *lchild, *rchild;// 左右孩子指针} BiTNode, *BiTree;lchild data rchild结点结构:C 语言的类型描述如下:ADEBCF root 2.三叉链表parent lchild data rchild结点结构:typedef struct TriTNode { // 结点结构TElemType data;struct TriTNode *lchild, *rchild;// 左右孩子指针struct TriTNode *parent; //双亲指针} TriTNode, *TriTree;parent lchild data rchild结点结构:C 语言的类型描述如下:0123456data parent结点结构:3.双亲链表LRTagLRRRLABDCEFtypedef struct BPTNode { // 结点结构TElemType data;int *parent; // 指向双亲的指针char LRTag; // 左、右孩子标志域} BPTNodetypedef struct BPTree{ // 树结构BPTNode nodes[MAX_TREE_SIZE];int num_node; // 结点数目int root; // 根结点的位置} BPTree6.4二叉树的遍历一、问题的提出二、先左后右的遍历算法三、算法的递归描述四、中序遍历算法的非递归描述五、遍历算法的应用举例顺着某一条搜索路径巡访二叉树中的结点,使得每个结点均被访问一次,而且仅被访问一次。一、问题的提出“访问”的含义可以很广,如:输出结点的信息等。“遍历”是任何类型均有的操作,对线性结构而言,只有一条搜索路径(因为每个结点均只有一个后继),故不需要另加讨论。而二叉树是非线性结构,每个结点有两个后继,则存在如何遍历即按什么样的搜索路径遍历的问题。对“二叉树”而言,可以有三条搜索路径:1.先上后下的按层次遍历;2.先左(子树)后右(子树)的遍历;3.先右(子树)后左(子树)的遍历。二、先左后右的遍历算法先(根)序的遍历算法中(根)序的遍历算法后(根)序的遍历算法根左子树右子树若二叉树为空树,则空操作;否则,(1)访问根结点;(2)先序遍历左子树;(3)先序遍历右子树。先(根)序的遍历算法:若二叉树为空树,则空操作;否则,(1)中序遍历左子树;(2)访问根结点;(3)中序遍历右子树。中(根)序的遍历算法:若二叉树为空树,则空操作;否则,(1)后序遍历左子树;(2)后序遍历右子树;(3)访问根结点。后(根)序的遍历算法:ABCDEFGHK例如:先序序列:中序序列:后序序列:A B C D E F G H KB D C A E H G K FD C B H K G F E A三、算法的递归描述void Preorder (BiTree T,void( *visit)(TElemType& e)){ // 先序遍历二叉树if (T) {visit(T->data); // 访问结点Preorder(T->lchild, visit); // 遍历左子树Preorder(T->rchild, visit);// 遍历右子树}}四、中序遍历算法的非递归描述有两种分析(描述)方法:一、“任务书”分析方法二、“路径”分析方法在写算法之前首先需定义栈的元素类型。typedef enum { Travel, Visit } TaskType;// Travel == 1:遍历, // Visit == 0:访问typedef struct {BiTree ptr; // 指向根结点的指针TaskType task; // 任务性质} ElemType;“遍历二叉树”包括三项子任务:“遍历左子树”“遍历右子树”“访问根结点”void InOrder_iter( BiTree BT ) {// 利用栈实现中序遍历二叉树,T为指向二叉树的根结点的头指针InitStack(S);e.ptr=BT; e.task=Travel;if(T) Push(S, e); // 布置初始任务while(!StackEmpty(S)) {Pop(S,e); // 每次处理一项任务if (e.task==Visit) visit(e.ptr); // 处理访问任务elseif(!e.ptr){ // 处理非空树的遍历任务p=e.ptr;e.ptr=p->rchild; Push(S,e);// 最不迫切任务进栈e.ptr=p; e.task=Visit; Push(S,e);e.ptr=p->lchild; e.task=Travel; Push(S,e);}//if} //while}//InOrder_itervoid Inorder_I(BiTree T, void (*visit)(TelemType& e)){Stack *S;t = GoFarLeft(T, S); // 找到最左下的结点while(t){visit(t->data);if (t->rchild)elseif ( !StackEmpty(S )) t = Pop(S); // 退栈else t = NULL; // 栈空表明遍历结束} // while}// Inorder_It = GoFarLeft(t->rchild, S);BiTNode *GoFarLeft(BiTree T, Stack *S){if (!T ) return NULL;while (T->lchild ){Push(S, T);T = T->lchild;}return T;}五、遍历算法的应用举例2、统计二叉树中叶子结点的个数3、求二叉树的深度(后序遍历)4、复制二叉树(后序遍历)5、建立二叉树的存储结构1、查询二叉树中某个结点Status Preorder (BiTree T, ElemType x, BiTree &p){// 若二叉树中存在和x相同的元素,则p指向该结点并返回trueif (T) {if (T->data==x) { p=T; return OK,}else {if (Preorder(T->lchild, x, p) return OK;else return(Preorder(T->rchild, x, p)) ;}//else}//ifelse return FALSE;}2、统计二叉树中叶子结点的个数算法基本思想:先序(或中序或后序)遍历二叉树,在遍历过程中查找叶子结点,并计数。由此,需在遍历算法中增添一个“计数”的参数,并将算法中“访问结点” 的操作改为:若是叶子,则计数器增1。void CountLeaf (BiTree T, int& count){if ( T ) {if ((!T->lchild)&& (!T->rchild))count++; // 对叶子结点计数CountLeaf( T->lchild, count);CountLeaf( T->rchild, count);} // if} // CountLeafint CountLeaf (BiTree T){if (!T ) return 0;if (!T->lchild && !T->rchild) return 1;else{m = CountLeaf( T->lchild);n = CountLeaf( T->rchild);return (m+n);} //else} // CountLeaf3、求二叉树的深度(后序遍历)算法基本思想:从二叉树深度的定义可知,二叉树的深度应为其左、右子树深度的最大值加1。由此,需先分别求得左、右子树的深度,算法中“访问结点”的操作为:求得左、右子树深度的最大值,然后加 1 。首先分析二叉树的深度和它的左、右子树深度之间的关系。int Depth (BiTree T ){ // 返回二叉树的深度if ( !T ) depthval = 0;else {depthLeft = Depth( T->lchild );depthRight= Depth( T->rchild );depthval = 1 + (depthLeft > depthRight depthLeft : depthRight);}return depthval;}void Depth(BiTree T , int level, int &dval){if ( T ) {if (level>dval) dval = level;Depth( T->lchild, level+1, dval );Depth( T->rchild, level+1, dval );}}// 调用之前 level 的初值为 1。// dval 的初值为0.4、复制二叉树其基本操作为:生成一个结点。根元素T左子树右子树根元素NEWT左子树右子树左子树右子树(后序遍历)BiTNode *GetTreeNode(TElemType item,BiTNode *lptr , BiTNode *rptr ){if (!(T = new BiTNode))exit(1);T-> data = item;T-> lchild = lptr; T-> rchild = rptr;return T;}生成一个二叉树的结点(其数据域为item,左指针域为lptr,右指针域为rptr)BiTNode *CopyTree(BiTNode *T) {if (!T ) return NULL;if (T->lchild )newlptr = CopyTree(T->lchild);//复制左子树else newlptr = NULL;if (T->rchild )newrptr = CopyTree(T->rchild);//复制右子树else newrptr = NULL;newT = GetTreeNode(T->data, newlptr, newrptr);return newT;} // CopyTreeABCDEFGHK^ D ^C ^^ B^ H ^^ K ^GF ^E ^A例如:下列二叉树的复制过程如下:newT5、建立二叉树的存储结构不同的定义方法相应有不同的存储结构的建立算法以字符串的形式 根 左子树 右子树定义一棵二叉树例如:ABCD以空白字符“ ”表示A(B( ,C( , )),D( , ))空树只含一个根结点的二叉树A以字符串“A ”表示以下列字符串表示Status CreateBiTree(BiTree &T) {scanf(&ch);if (ch==' ') T = NULL;else {if (!(T = new BiTNode))exit(OVERFLOW);T->data = ch; // 生成根结点CreateBiTree(T->lchild); // 构造左子树CreateBiTree(T->rchild); // 构造右子树}return OK; } // CreateBiTreeA B C DABCD上页算法执行过程举例如下:ATBCD^^^^^按给定的表达式建相应二叉树 由先缀表示式建树例如:已知表达式的先缀表示式-×+ a b c / d e 由原表达式建树例如:已知表达式 (a+b)×c – d/e对应先缀表达式 -×+ a b c / d e的二叉树abcde-×+/特点:操作数为叶子结点,运算符为分支结点scanf(&ch);if ( In(ch, 字母集 )) 建叶子结点;else { 建根结点;递归建左子树;递归建右子树;}由先缀表示式建树的算法的基本操作:a+b(a+b)×c – d/ea+b×c分析表达式和二叉树的关系:ab+abc×+abc×+(a+b)×cabcde-×+/基本操作:scanf(&ch);if (In(ch, 字母集 )) { 建叶子结点; 暂存; }else if (In(ch, 运算符集)){ 和前一个运算符比较优先数;若当前的优先数“高”,则暂存;否则建子树;}void CrtExptree(BiTree &T, char exp[] ) {InitStack(S); Push(S, # ); InitStack(PTR);p = exp; ch = *p;while (!(GetTop(S)== # && ch== # )) {if (!IN(ch, OP)) CrtNode( t, ch );// 建叶子结点并入栈else { }if ( ch!= # ) { p++; ch = *p;}} // whilePop(PTR, T);} // CrtExptree… …switch (ch) {case ( : Push(S, ch); break;case ) : Pop(S, c);while (c!= ( ) {CrtSubtree( t, c); // 建二叉树并入栈Pop(S, c) }break;defult :} // switch… …while(!Gettop(S, c) && ( precede(c,ch))) {CrtSubtree( t, c);Pop(S, c);}if ( ch!= # ) Push( S, ch);break;建叶子结点的算法为:void CrtNode(BiTree& T,char ch){if (!(T= new BiTNode))exit(OVERFLOW);T->data = char;T->lchild = T->rchild = NULL;Push( PTR, T );}建子树的算法为:void CrtSubtree (Bitree& T, char c){if (!(T= new BiTNode))exit(OVERFLOW);T->data = c;Pop(PTR, rc); T->rchild = rc;Pop(PTR, lc); T->lchild = lc;Push(PTR, T);}仅知二叉树的先序序列“abcdefg” 不能唯一确定一棵二叉树,由二叉树的先序和中序序列建树如果同时已知二叉树的中序序列“cbdaegf”,则会如何?二叉树的先序序列二叉树的中序序列左子树左子树右子树右子树根根a b c d e f gc b d a e g f例如:aabbccddeeffggabcdefg^^^^^^^^先序序列中序序列void CrtBT(BiTree& T, char pre[], char ino[],int ps, int is, int n ) {// 已知pre[ps..ps+n-1]为二叉树的先序序列,// ins[is..is+n-1]为二叉树的中序序列,本算// 法由此两个序列构造二叉链表if (n==0) T=NULL;else {k=Search(ino, pre[ps]); // 在中序序列中查询if (k== -1) T=NULL;else { }} //} // CrtBT… …if (!(T= new BiTNode)) exit(OVERFLOW);T->data = pre[ps];if (k==is) T->Lchild = NULL;else CrtBT(T->Lchild, pre[], ino[],ps+1, is, k-is );if (k=is+n-1) T->Rchild = NULL;else CrtBT(T->Rchild, pre[], ino[],ps+1+(k-is), k+1, n-(k-is)-1 );6.5线索二叉树何谓线索二叉树?线索链表的遍历算法如何建立线索链表?一、何谓线索二叉树?遍历二叉树的结果是,求得结点的一个线性序列。ABCDEFGHK例如:先序序列:A B C D E F G H K中序序列:B D C A H G K F E后序序列:D C B H K G F E A指向该线性序列中的“前驱”和“后继” 的指针,称作“线索”与其相应的二叉树,称作 “线索二叉树”包含 “线索” 的存储结构,称作 “线索链表”A B C D E F G H K^ D ^C ^^ BE ^对线索链表中结点的约定:在二叉链表的结点中增加两个标志域,并作如下规定:若该结点的左子树不空,则Lchild域的指针指向其左子树,且左标志域的值为“指针 Link”;否则,Lchild域的指针指向其“前驱”,且左标志的值为“线索 Thread” 。若该结点的右子树不空,则rchild域的指针指向其右子树,且右标志域的值为 “指针 Link”;否则,rchild域的指针指向其“后继”,且右标志的值为“线索 Thread”。如此定义的二叉树的存储结构称作“线索链表”typedef struct BiThrNod {TElemType data;struct BiThrNode *lchild, *rchild; // 左右指针PointerThr LTag, RTag; // 左右标志} BiThrNode, *BiThrTree;线索链表的类型描述:typedef enum { Link, Thread } PointerThr;// Link==0:指针,Thread==1:线索二、线索链表的遍历算法:for ( p = firstNode(T); p; p = Succ(p) )Visit (p);由于在线索链表中添加了遍历中得到的“前驱”和“后继”的信息,从而简化了遍历的算法。例如:对中序线索化链表的遍历算法※ 中序遍历的第一个结点 ?※ 在中序线索化链表中结点的后继 ?左子树上处于“最左下”(没有左子树)的结点若无右子树,则为后继线索所指结点否则为对其右子树进行中序遍历时访问的第一个结点void InOrderTraverse_Thr(BiThrTree T,void (*Visit)(TElemType e)) {p = T->lchild; // p指向根结点while (p != T) { // 空树或遍历结束时,p==Twhile (p->LTag==Link) p = p->lchild; // 第一个结点while (p->RTag==Thread && p->rchild!=T) {p = p->rchild; Visit(p->data); // 访问后继结点}p = p->rchild; // p进至其右子树根}} // InOrderTraverse_Thr在中序遍历过程中修改结点的左、右指针域,以保存当前访问结点的“前驱”和“后继”信息。遍历过程中,附设指针pre, 并始终保持指针pre指向当前访问的、指针p所指结点的前驱。三、如何建立线索链表?void InThreading(BiThrTree p) {if (p) { // 对以p为根的非空二叉树进行线索化InThreading(p->lchild); // 左子树线索化if (!p->lchild) // 建前驱线索{ p->LTag = Thread; p->lchild = pre; }if (!pre->rchild) // 建后继线索{ pre->RTag = Thread; pre->rchild = p; }pre = p; // 保持 pre 指向 p 的前驱InThreading(p->rchild); // 右子树线索化} // if} // InThreadingStatus InOrderThreading(BiThrTree &Thrt,BiThrTree T) { // 构建中序线索链表if (!(Thrt = new BiThrNode) )exit (OVERFLOW);Thrt->LTag = Link; Thrt->RTag =Thread;Thrt->rchild = Thrt; // 添加头结点return OK;} // InOrderThreading… …if (!T) Thrt->lchild = Thrt;else {Thrt->lchild = T; pre = Thrt;InThreading(T);pre->rchild = Thrt; // 处理最后一个结点pre->RTag = Thread;Thrt->rchild = pre;}6.6 树和森林的表示方法树的三种存储结构一、双亲表示法二、孩子链表表示法三、树的二叉链表(孩子-兄弟)存储表示法ABCDEFGr=0n=60 A -11 B 02 C 03 D 04 E 25 F 26 G 5data parent一、双亲表示法:typedef struct PTNode {Elem data;int parent; // 双亲位置域} PTNode;data parent#define MAX_TREE_SIZE 100结点结构:C语言的类型描述:typedef struct {PTNode nodes[MAX_TREE_SIZE];int r, n;// 根结点的位置和结点个数} PTree;树结构:r=0n=6data firstchildABCDEFG0 A -11 B 02 C 03 D 04 E 25 F 26 G 464 51 2 3二、孩子链表表示法:-1000224typedef struct CTNode {int child;struct CTNode *nextchild;} *ChildPtr;孩子结点结构:child nextchildC语言的类型描述:typedef struct {Elem data;ChildPtr firstchild;// 孩子链的头指针} CTBox;双亲结点结构data firstchildtypedef struct {CTBox nodes[MAX_TREE_SIZE];int n, r;// 结点数和根结点的位置} CTree;树结构:ABCDEFGrootABCE DFGABCE DFG三、树的二叉链表 (孩子-兄弟)存储表示法roottypedef struct CSNode{Elem data;struct CSNode*firstchild, *nextsibling;} CSNode, *CSTree;C语言的类型描述:结点结构:firstchild data nextsibling森林和二叉树的对应关系设森林F = ( T1, T2, …, Tn );T1 = ( root,t11, t12, …, t1m );二叉树B =( LBT, Node(root), RBT );由森林转换成二叉树的转换规则为:若 F = Φ,则 B = Φ;由 ROOT( T1 ) 对应得到Node(root);否则,由 (t11, t12, …, t1m ) 对应得到 LBT;由 (T2, T3,…, Tn ) 对应得到 RBT。由二叉树转换为森林的转换规则为:由LBT 对应得到 ( t11, t12, …,t1m);若 B = Φ, 则 F = Φ;否则,由 Node(root) 对应得到 ROOT( T1 );由RBT 对应得到 (T2, T3, …, Tn)。T1T11,T12,…,T1mT2,…,TnLBTRBTroot由此,树和森林的各种操作均可与二叉树的各种操作相对应。应当注意的是,和树对应的二叉树,其左、右子树的概念已改变为: 左是孩子,右是兄弟6.7树和森林的遍历一、树的遍历二、森林的遍历三、树的遍历的应用树的遍历可有三条搜索路径:按层次遍历:先根(次序)遍历:后根(次序)遍历:若树不空,则先访问根结点,然后依次先根遍历各棵子树。若树不空,则先依次后根遍历各棵子树,然后访问根结点。若树不空,则自上而下自左至右访问树中每个结点。层次遍历时顶点的访问次序:AB C DE F GHI J K先根遍历时顶点的访问次序:A B E F C D G H I J K后根遍历时顶点的访问次序:E F B C I J K H G D AA B C D E F G H I J KB C DE F GHI J K1。森林中第一棵树的根结点;2。森林中第一棵树的子树森林;3。森林中其它树构成的森林。可以分解成三部分:森林若森林不空,则访问森林中第一棵树的根结点;先序遍历森林中第一棵树的子树森林;先序遍历森林中(除第一棵树之外)其余树构成的森林。先序遍历森林的遍历即:依次从左至右对森林中的每一棵树进行先根遍历。中序遍历若森林不空,则中序遍历森林中第一棵树的子树森林;访问森林中第一棵树的根结点;中序遍历森林中(除第一棵树之外)其 余树构成的森林。即:依次从左至右对森林中的每一棵树进行后根遍历。树的遍历和二叉树遍历的对应关系 ?先根遍历后根遍历树二叉树森林先序遍历先序遍历中序遍历中序遍历设树的存储结构为孩子兄弟链表typedef struct CSNode{Elem data;struct CSNode *firstchild, *nextsibling;} CSNode, *CSTree;一、求树的深度二、输出树中所有从根到叶子的路径三、建树的存储结构int TreeDepth( Ctree T ){ // T 是树的孩子链表存储结构,// 返回该树的深度if ( T.n == 0) return 0;elsereturn Depth( T, T.r );} // TreeDepth一、求树的深度的算法:int Depth( Ctree T, int root ){max = 0;p = T.nodes[root].firstchild;while ( p ) {h = Depth( T, p->child );if ( h > max ) max = h;p = p->nextchild;}return max+1;}二、输出树中所有从根到叶子的路径的算法:AB C DE F GHI J K例如:对左图所示的树,其输出结果应为:A B EA B FA CA D G H IA D G H JA D G H Kvoid AllPath( BiTree T, Stack& S ) {if (T) {Push( S, T->data );if (!T->Lchild && !T->Rchild ) PrintStack(S);else { AllPath( T->Lchild, S );AllPath( T->Rchild, S );}Pop(S);} // if(T)} // AllPath// 输出二叉树上从根到所有叶子结点的路径void OutPath( Bitree T, Stack& S ) {while ( !T ) {Push(S, T->data );if ( !T->firstchild ) Printstack(S);else OutPath( T->firstchild, S );Pop(S);T = T->nextsibling;} // while} // OutPath// 输出森林中所有从根到叶的路径三、建树的存储结构的算法:和二叉树类似,不同的定义相应有不同的算法。假设以二元组(F,C)的形式自上而下、自左而右依次输入树的各边,建立树的孩子-兄弟链表。ABCDEFG例如:对下列所示树的输入序列应为:(‘#’, ‘A’)(‘A’, ‘B’)(‘A’, ‘C’)(‘A’, ‘D’)(‘C’, ‘E’)(‘C’, ‘F’)(‘E’, ‘G’)ABCD(‘#’, ‘A’)(‘A’, ‘B’)(‘A’, ‘C’)(‘A’, ‘D’)(‘C’, ‘E’)可见,算法中需要一个队列保存已建好的结点的指针void CreatTree( CSTree &T ) {T = NULL;for( scanf(&fa, &ch); ch!= ;scanf(&fa, &ch);) {p = GetTreeNode(ch); // 创建结点EnQueue(Q, p); // 指针入队列if (fa == ) T = p; // 所建为根结点else { } // 非根结点的情况} // for} // CreateTree… …GetHead(Q,s); // 取队列头元素(指针值)while (s->data != fa ) { // 查询双亲结点DeQueue(Q,s); GetHead(Q,s);}if (!(s->firstchild)){ s->firstchild = p; r = p; }// 链接第一个孩子结点else { r->nextsibling = p; r = p; }// 链接其它孩子结点6.8 哈 夫 曼 树 与哈 夫 曼 编 码最优树的定义如何构造最优树前缀编码一、最优树的定义树的路径长度定义为:树中每个结点的路径长度之和。结点的路径长度定义为:从根结点到该结点的路径上分支的数目。树的带权路径长度定义为:树中所有叶子结点的带权路径长度之和WPL(T) = wklk (对所有叶子结点)在所有含 n 个叶子结点、并带相同权值的 m 叉树中,必存在一棵其带权路径长度取最小值的树,称为“最优树”。例如:27 975492WPL(T)= 7 2+5 2+2 3+4 3+9 2 =60WPL(T)= 7 4+9 4+5 3+4 2+2 1 =8954根据给定的 n 个权值 {w1, w2, …, wn},构造 n 棵二叉树的集合F = {T1, T2, … , Tn},其中每棵二叉树中均只含一个带权值为 wi 的根结点,其左、右子树为空树;二、如何构造最优树(1)(赫夫曼算法) 以二叉树为例:在 F 中选取其根结点的权值为最小的两棵二叉树,分别作为左、右子树构造一棵新的二叉树,并置这棵新的二叉树根结点的权值为其左、右子树根结点的权值之和;(2)从F中删去这两棵树,同时加入刚生成的新树;重复 (2) 和 (3) 两步,直至 F 中只含一棵树为止。(3)(4)9例如: 已知权值 W={ 5, 6, 2, 9, 7 }5627527697671395276713952795271667132900001111000111100101指的是,任何一个字符的编码都不是同一字符集中另一个字符的编码的前缀。三、前缀编码利用赫夫曼树可以构造一种不等长的二进制编码,并且构造所得的赫夫曼编码是一种最优前缀编码,即使所传电文的总长度最短。 1. 熟练掌握二叉树的结构特性,了解相应的证明方法。 2. 熟悉二叉树的各种存储结构的特点及适用范围。 3. 遍历二叉树是二叉树各种操作的基础。实现二叉树遍历的具体算法与所采用的存储结构有关。掌握各种遍历策略的递归算法,灵活运用遍历算法实现二叉树的其它操作。层次遍历是按另一种搜索策略进行的遍历。 4. 理解二叉树线索化的实质是建立结点与其在相应序列中的前驱或后继之间的直接联系,熟练掌握二叉树的线索化过程以及在中序线索化树上找给定结点的前驱和后继的方法。二叉树的线索化过程是基于对二叉树进行遍历,而线索二叉树上的线索又为相应的遍历提供了方便。 5. 熟悉树的各种存储结构及其特点,掌握树和森林与二叉树的转换方法。建立存储结构是进行其它操作的前提,因此读者应掌握 1 至 2 种建立二叉树和树的存储结构的方法。 6. 学会编写实现树的各种操作的算法。 7. 了解最优树的特性,掌握建立最优树和哈夫曼编码的方法。QUICK QUIZ(6)一、基础知识题1. 就如图5.21所示的树回答下面问题:1)哪个是根结点?2)哪些是叶子结点?3)哪个是E的父结点?4)哪些是E的子孙结点?5)哪些是E的兄弟结点?哪些是C的兄弟结点?6)结点B和结点I的层数分别是多少?7)树的深度是多少?8)以结点G为根的子树的深度是多少?9)树的度是多少?图5.21 树的例子2. 分别画出含3个结点的树与二叉树的所有不同形态。3. 高度为h的完全二叉树至少有多少个结点?最多有多少个结点?4. 采用顺序存储方法和链式存储方法分别画出图5.22所示二叉树的存储结构。5. 分别写出图5.22所示二叉树的前序、中序和后序遍历序列。图5.22 二叉树例子6. 若二叉树中各结点值均不相同。1)已知一个二叉树的中序和后序遍历序列分别为GDHBAECIF和GHDBEIFCA,请画出此二叉树。2)已知一个二叉树的前序和中序分别为ABCDEFGH和BDCEAFHG,请画出此二叉树。7. 一个二叉树如图5.23所示,分别写出其前序、中序、后序的遍历序列。8. 输入一个正整数序列{55,34,18,88,119,11,76,9,97,99,46},试构造一个二叉排序树。9. 有一份电文中共使用5个字符:a、b、c、d、e,它们的出现频率依次为5、2、1、6、4。试画出对应的哈夫曼树,并求出每个字符的哈夫曼编码。图5.23 一个二叉树二、算法设计题1. 一个二叉树以链式结构存储,分别给出求二叉树结点总数和叶子结点总数的算法。2. 一个二叉树以链式结构存储,写出在二叉树中查找值为x的结点的算法。3. 设计一个算法将一个以链式存储结构的二叉树,按顺序方式存储到一维数组中。4. 假设二叉排序树t的各元素值均不相同,设计一个算法按递增次序打印各元素值。5. 已知一个中序线索二叉树,试编写中序遍历的非递归算法。返回习题解答一、基本知识题答案1. 答:1)A是根结点2)D、H、I、J、F、G是叶子结点3)B是E的父结点4)H、I、J是E的子孙结点5)D是E的兄弟结点,B是C的兄弟结点6)B的层数是2,I的层数是47)树的深度是48)以结点G为根的子树的深度是19)树的度是32. 答:3个结点的树:3个结点的二叉树:3. 答:高度为h的完全二叉树至少有个结点,最多有个结点。4. 答:该二叉树的顺序存储:该二叉树的链式存储:5. 答:先序遍历序列:1、2、4、7、3、5、8、6、9中序遍历序列:7、4、2、1、8、5、3、6、9后序遍历序列:7、4、2、8、5、9、6、3、16.答:(1)(2)7.答:先序遍历序列:A、B、D、E、F、C、G中序遍历序列:D、F、E、B、A、G、C后序遍历序列:F、E、D、B、G、C、A8.答:二叉排序树如下:9. 答:本题对应的对应的哈夫曼树如下:各字符对应的哈夫曼编码如下:a: 10, b: 001, c: 000, d: 11, e: 01。二、算法设计题答案1. 解:求二叉树结点总数的算法如下:int CountNode(btree *t,int num) /*num初值为0*/{ if(t!=NULL){num++;num=CountNode(t->left,num);num=CountNode(t->right,num);}return num;}求二叉树叶子结点总数的算法如下:int CountLeaf(btree *t,int num) /*num初值为0*/{ if(t!=NULL){if(t->left==NULL && t->right==NULL)num++;num=CountLeaf(t->left,num);num=CountLeaf(t->right,num);}return num;}2. 解:本题可以用先序、中序和后序遍历中的任意一种遍历,只要将遍历算法中的访问根结点改为判断其值是否等于x。下面是用中序遍历求解的算法,函数返回值为x的结点的地址,若没有找到则返回空。btree *search(btree *t,int x,btree p)/*p的初值为NULL*/{if(t!=NULL){p=search(t->left,x,p);if(t->data==x)p=t; /*找到x的地址放在p中*/p=search(t->right,x,p);}return p;}3. 解:这是一个递归算法,如下:void create(btree *t,int tree[],int i){if(t!=NULL){tree[i]=t->data;create(t->left,tree,2*i);create(t->right,tree,2*i+1);}}4. 解:按中序序列遍历二叉排序树即按递增次序遍历,所以递增打印二叉排序树各元素值的函数如下:void inorder(btree *t){if(t!=NULL){inorder(t->left);printf("%d",t->data);inorder(t->right);}}5. 解:在中序线索二叉树中进行非递归中序遍历,只要从头结点出发,反复找到结点的后继,直至结束即可。在中序线索二叉树中求结点后继的算法如下:tbtree *succ(tbtree *p){btree *q;if(p->rtag==1)return (p->right);else{q=p->right;while(q->ltag==0)q=q->left;return(q);}}由此得到中序遍历线索二叉树的非递归算法如下:void thinorder(tbtree *p){ if(p!=NULL){while(p->ltag==0)p=p->left;do{printf("%d",p->data);p=succ(p);}while(p!=NULL);}}返回(共107张PPT)通常称,栈和队列是限定插入和删除只能在表的“端点”进行的线性表。线性表 栈 队列Insert(L, i, x) Insert(S, n+1, x) Insert(Q, n+1, x)1≤i≤n+1Delete(L, i) Delete(S, n) Delete(Q, 1)1≤i≤n栈和队列是两种常用的数据类型3.1 栈的类型定义3.2 栈的应用举例3.3 栈类型的实现3.4 队列的类型定义3.5 队列类型的实现3.1 栈的类型定义ADT Stack {数据对象:D={ ai | ai ∈ElemSet, i=1,2,...,n, n≥0 }数据关系:R1={ | ai-1, ai∈D, i=2,...,n }约定an 端为栈顶,a1 端为栈底。基本操作:} ADT StackInitStack(&S)DestroyStack(&S)ClearStack(&S)StackEmpty(s)StackLength(S)GetTop(S, &e)Push(&S, e)Pop(&S, &e)StackTravers(S, visit())InitStack(&S)操作结果:构造一个空栈 S。DestroyStack(&S)初始条件:栈 S 已存在。操作结果:栈 S 被销毁。StackEmpty(S)初始条件:栈 S 已存在。操作结果:若栈 S 为空栈,则返回 TRUE,否则 FALE。StackLength(S)初始条件:栈 S 已存在。操作结果:返回 S 的元素个数,即栈的长度。GetTop(S, &e)初始条件:栈 S 已存在且非空。操作结果:用 e 返回 S 的栈顶元素。a1a2an… …ClearStack(&S)初始条件:栈 S 已存在。操作结果:将 S 清为空栈。Push(&S, e)初始条件:栈 S 已存在。操作结果:插入元素 e 为新的栈顶元素。a1a2ane… …Pop(&S, &e)初始条件:栈 S 已存在且非空。操作结果:删除 S 的栈顶元素,并用 e 返回其值。a1a2anan-1… …3.2 栈的应用举例例一、 数制转换例二、 括号匹配的检验例三、 行编辑程序问题例四、 迷宫求解例五、 表达式求值例六、 实现递归例一、 数制转换算法基于原理:N = (N div d)×d + N mod d例如:(1348)10 = (2504)8 ,其运算过程如下:N N div 8 N mod 81348 168 4168 21 021 2 52 0 2计算顺序输出顺序void conversion () {InitStack(S);scanf ("%d",N);while (N) {Push(S, N % 8);N = N/8;}while (!StackEmpty(S)) {Pop(S,e);printf ( "%d", e );}} // conversion例二、 括号匹配的检验假设在表达式中([]())或[([ ][ ])]等为正确的格式,[( ])或([( ))或 (( )])均为不正确的格式。则 检验括号是否匹配的方法可用“期待的急迫程度”这个概念来描述。分析可能出现的不匹配的情况:到来的右括弧非是所“期待”的;例如:考虑下列括号序列:[ ( [ ] [ ] ) ]1 2 3 4 5 6 7 8到来的是“不速之客”;直到结束,也没有到来所“期待”的括弧;算法的设计思想:1)凡出现左括弧,则进栈;2)凡出现右括弧,首先检查栈是否空若栈空,则表明该“右括弧”多余否则和栈顶元素比较,若相匹配,则“左括弧出栈”否则表明不匹配3)表达式检验结束时,若栈空,则表明表达式中匹配正确否则表明“左括弧”有余Status matching(string& exp) {int state = 1; initstack(s);while (i<=Length(exp) && state) {switch of exp[i] {case 左括弧:{Push(S,exp[i]); i++; break;}case”)”: {if(NOT StackEmpty(S)&&GetTop(S)=“(“{Pop(S,e); i++;}else {state = 0;}break; } … …}if (StackEmpty(S)&&state) return OK; …...例三、行编辑程序问题如何实现?“每接受一个字符即存入存储器” 并不恰当!设立一个输入缓冲区,用以接受用户输入的一行字符,然后逐行存入用户数据区; 并假设“#”为退格符,“@”为退行符。在用户输入一行的过程中,允许用户输入出差错,并在发现有误时可以及时更正。合理的作法是:假设从终端接受了这样两行字符:whli##ilr#e(s#*s)outcha@putchar(*s=#++);则实际有效的是下列两行:while (*s)putchar(*s++);while (ch != EOF && ch != '\n') {switch (ch) {case '#' : Pop(S, c); break;case '@': ClearStack(S); break;// 重置S为空栈default : Push(S, ch); break;}ch = getchar(); // 从终端接收下一个字符}ClearStack(S); // 重置S为空栈if (ch != EOF) ch = getchar(); }while (ch != EOF) { //EOF为全文结束符将从栈底到栈顶的字符传送至调用过程的数据区;例四、 迷宫求解通常用的是“穷举求解”的方法 1 1 11 2 22 2 23 2 13 3 13 4 42 4 12 5 12 6 41 6 31 5 31 4 43$$$$$$$$求迷宫路径算法的基本思想是:若当前位置“可通”,则纳入路径,继续前进;若当前位置“不可通”,则后退,换方向继续探索;若四周“均无通路”,则将当前位置从路径中删除出去。设定当前位置的初值为入口位置;do{若当前位置可通,则{将当前位置插入栈顶;若该位置是出口位置,则算法结束;否则切换当前位置的东邻方块为新的当前位置;}否则 {}}while (栈不空);求迷宫中一条从入口到出口的路径的算法:… …若栈不空且栈顶位置尚有其他方向未被探索,则设定新的当前位置为: 沿顺时针方向旋转找到的栈顶位置的下一相邻块;若栈不空但栈顶位置的四周均不可通,则{删去栈顶位置;// 从路径中删去该通道块若栈不空,则重新测试新的栈顶位置,直至找到一个可通的相邻块或出栈至栈空;}若栈空,则表明迷宫没有通路。限于二元运算符的表达式定义:表达式 ::= (操作数) + (运算符) + (操作数)操作数 ::= 简单变量 | 表达式简单变量 :: = 标识符 | 无符号整数例五、 表达式求值表达式的三种标识方法:设 Exp = S1 + OP + S2则称 OP + S1 + S2 为前缀表示法S1 + OP + S2 为中缀表示法S1 + S2 + OP 为后缀表示法例如: Exp = a b + (c d / e) f前缀式: + a b c / d e f中缀式: a b + c d / e f后缀式: a b c d e / f +结论:1)操作数之间的相对次序不变;2)运算符的相对次序不同;3)中缀式丢失了括弧信息,致使运算的次序不确定;4)前缀式的运算规则为:连续出现的两个操作数和在它们之前且紧靠它们的运算符构成一个最小表达式;5)后缀式的运算规则为:运算符在式中出现的顺序恰为表达式的运算顺序; 每个运算符和在它之前出现 且紧靠它的两个操作数构成一个最小表达式;如何从后缀式求值?先找运算符,再找操作数例如:a b c d e / f +a bd/ec-d/e(c-d/e) f如何从原表达式求得后缀式?每个运算符的运算次序要由它之后的一个运算符来定,在后缀式中,优先数高的运算符领先于优先数低的运算符。分析 “原表达式” 和 “后缀式”中的运算符:原表达式: a + b c d / e f后缀式: a b c + d e / f 从原表达式求得后缀式的规律为:1) 设立暂存运算符的栈;2) 设表达式的结束符为“#”,予设运算符栈的栈底为“#”3) 若当前字符是操作数,则直接发送给后缀式;4) 若当前运算符的优先数高于栈顶运算符,则进栈;5) 否则,退出栈顶运算符发送给后缀式;6) “(” 对它之前后的运算符起隔离作用,“)”可视为自相应左括弧开始的表达式的结束符。从原表达式求得后缀式的规律为:void transform(char suffix[ ], char exp[ ] ) {InitStack(S); Push(S, # );p = exp; ch = *p;while (!StackEmpty(S)) {if (!IN(ch, OP)) Pass( Suffix, ch);else { }if ( ch!= # ) { p++; ch = *p; }else { Pop(S, ch); Pass(Suffix, ch); }} // while} // CrtExptree… …switch (ch) {case ( : Push(S, ch); break;case ) : Pop(S, c);while (c!= ( ){ Pass( Suffix, c); Pop(S, c) }break;defult :while(!Gettop(S, c) && ( precede(c,ch))){ Pass( Suffix, c); Pop(S, c); }if ( ch!= # ) Push( S, ch);break;} // switch例六、实现递归将所有的实在参数、返回地址等信息传递给被调用函数保存;为被调用函数的局部变量分配存储区;将控制转移到被调用函数的入口。当在一个函数的运行期间调用另一个函数时,在运行该被调用函数之前, 需先完成三项任务:保存被调函数的计算结果;释放被调函数的数据区;依照被调函数保存的返回地址将控制转移到调用函数。从被调用函数返回调用函数之前,应该完成下列三项任务:递归工作栈:递归过程执行过程中占用的数据区。递归工作记录:每一层的递归参数合成一个记录。当前活动记录:栈顶记录指示当前层的执行情况。当前环境指针:递归工作栈的栈顶指针。递归函数执行的过程可视为同一函数进行嵌套调用,例如:多个函数嵌套调用的规则是:此时的内存管理实行“栈式管理”后调用先返回 !例如:void main( ){ void a( ){ void b( ){… … …a( ); b( );… …}//main }// a }// bMain的数据区函数a的数据区函数b的数据区void hanoi (int n, char x, char y, char z) {// 将塔座x上按直径由小到大且至上而下编号为1至n// 的n个圆盘按规则搬到塔座z上,y可用作辅助塔座。1 if (n==1)2 move(x, 1, z); // 将编号为1的圆盘从x移到z3 else {4 hanoi(n-1, x, z, y); // 将x上编号为1至n-1的//圆盘移到y, z作辅助塔5 move(x, n, z); // 将编号为n的圆盘从x移到z6 hanoi(n-1, y, x, z); // 将y上编号为1至n-1的//圆盘移到z, x作辅助塔7 }8 }8 3 a b c返址 n x y z5 2 a c b5 1 a b c7 1 c a bvoid hanoi (int n, char x, char y, char z) {1 if (n==1)2 move(x, 1, z);3 else {4 hanoi(n-1, x, z, y);5 move(x, n, z);6 hanoi(n-1, y, x, z);7 }8 }3.3 栈类型的实现顺序栈链栈//----- 栈的顺序存储表示 -----#define STACK_INIT_SIZE 100;typedef struct {SElemType *base;SElemType *top;int stacksize;} SqStack;类似于线性表的顺序映象实现,指向表尾的指针可以作为栈顶指针。Status InitStack (SqStack &S, int maxsize){// 构造一个最大空间为 maxsize 的空顺序栈 SS.base = new ElemType[maxsize];if (!S.base) exit (OVERFLOW); //存储分配失败S.top = S.base;S.stacksize = maxsize;return OK;}Status Push (SqStack &S, SElemType e){if (S.top - S.base >= S.stacksize) //栈满return OVERFLOW;*S.top++ = e;return OK;}Status Pop (SqStack &S, SElemType &e) {// 若栈不空,则删除S的栈顶元素,// 用 e 返回其值,并返回OK;// 否则返回ERRORif (S.top == S.base) return ERROR;e = *--S.top;return OK;}栈顶指针链栈∧a1an注意: 链栈中指针的方向an-1ADT Queue {数据对象:D={ai | ai∈ElemSet, i=1,2,...,n, n≥0}数据关系:R1={ | ai-1, ai ∈D, i=2,...,n}约定其中a1 端为队列头, an 端为队列尾基本操作:3.4 队列的类型定义} ADT Queue队列是一种运算受限制的线性表,元素的添加在表的一端进行,而元素的删除在表的另一端进行。允许添加元素的一端称为队尾(Rear);允许删除元素的一端称为队头(Front)。向队列添加元素称为入队,从队列中删除元素称为出队。新入队的元素只能添加在队尾,出队的元素只能是删除队头的元素,队列的特点是先进入队列的元素先出队,所以队列也称作先进先出表或FIFO(First-In-First-Out)表。队列的基本操作:InitQueue(&Q)DestroyQueue(&Q)QueueEmpty(Q)QueueLength(Q)GetHead(Q, &e)ClearQueue(&Q)DeQueue(&Q, &e)EnQueue(&Q, e)QueueTravers(Q, visit())InitQueue(&Q)操作结果:构造一个空队列QDestroyQueue(&Q)初始条件:队列Q已存在。操作结果:队列Q被销毁,不再存在。QueueEmpty(Q)初始条件:队列Q已存在。操作结果:若Q为空队列,则返回TRUE,否则返回FALSE。QueueLength(Q)初始条件:队列Q已存在。操作结果:返回Q的元素个数,即队列的长度。GetHead(Q, &e)初始条件:Q为非空队列。操作结果:用e返回Q的队头元素。a1a2an… …ClearQueue(&Q)初始条件:队列Q已存在。操作结果:将Q清为空队列。EnQueue(&Q, e)初始条件:队列Q已存在。操作结果:插入元素e为Q的新的队尾元素。a1a2ane… …DeQueue(&Q, &e)初始条件:Q为非空队列。操作结果:删除Q的队头元素,并用e返回其值。a1a2an… …3.5 队列类型的实现链队列——链式映象循环队列——顺序映象typedef struct QNode {// 结点类型QElemType data;struct QNode *next;} QNode, *QueuePtr;链队列——链式映象typedef struct { // 链队列类型QueuePtr front; // 队头指针QueuePtr rear; // 队尾指针} LinkQueue;a1∧an…Q.frontQ.rearQ.frontQ.rear∧空队列Status InitQueue (LinkQueue &Q) {// 构造一个空队列QQ.front = Q.rear = new QNode;if (!Q.front) exit (OVERFLOW);//存储分配失败Q.front->next = NULL;return OK;}Status EnQueue (LinkQueue &Q,QElemType e) {// 插入元素e为Q的新的队尾元素p = new QNode;if (!p) exit (OVERFLOW); //存储分配失败p->data = e; p->next = NULL;Q.rear->next = p; Q.rear = p;return OK;}Status DeQueue (LinkQueue &Q,QElemType &e) {// 若队列不空,则删除Q的队头元素,//用 e 返回其值,并返回OK;否则返回ERRORif (Q.front == Q.rear) return ERROR;p = Q.front->next; e = p->data;Q.front->next = p->next;delete (p); return OK;}if (Q.rear == p) Q.rear = Q.front;队列的顺序表示与堆栈类似,队列也可以简单的用一维数组表示。设数组名为Queue,其下标下界为0,上界为n-1。一般使用一个变量r指示队尾的下一个位置的下标值,叫做队尾指针;用另一个变量f指示队头的下标值,称为队头指针。队列中元素的数目等于零称为空队列,此时队头指针和队尾指针均为零,即f=r=0。假定有A~E 5个元素先后进入队列,但A、B两个元素已陆续出队了,故队尾指针r=6,而队头指针f=3。1. 入队(insert)当给队列插入元素时,队尾指针r后移而队头指针不动,但有一个情况例外,即当向空队列插入第一个元素时,队头指针与队尾指针同时由0变为1。设用下标从1到n的数组Q表示队列,且已知待添加的元素在变量x中。入队函数void insert (Q, int n, f, r, x){if (r == n-1)printf(“溢出!\n”);/*判断是否已到数组末端*/else{r=r+1;Q[r]=x; /*插入元素*/if (f == 0)f=1; /*判断原来是否为空队列*/}}2. 出队(Delete)当从队列删除元素时,队头指针f后移而队尾指针r不动,但也有一个情况例外,即当删除了最后一个元素,队列成为了空队列时,队头指针与队尾指针同时变为0。假设要求将出队的元素值赋给变量x 。出队函数void Delete (Q, int f, r, n, x){ if (f==0)printf(“下溢出!\n”); /*判断是否为空队列*/else{ x=Q[f]; /*取队头元素给x赋值*/if (f==r){ f=0;/*若出队的是最后一个元素,变成空队列*/r=0;}elsef=f+1; /*队头指针后移*/}}3. 队列存在的问题由于队列的入队操作是在两端进行的,随着元素的不断插入,删除,两端都向后移动,队列会很快移动到数组末端造成溢出,而前面的单元无法利用。解决办法:1) 每次删除一个元素后,将整个队列向前移动一个单元,保持队列头总固定在数组的第一个单元 。2) 将所用的数组想象成是头尾相接的圆环,当队列的尾端到达数组的末端(第n个单元)时,如果再插入元素可继续使队列向数组的前端(第1个单元)延长 ,此队列称为循环队列。#define MAXQSIZE 100 //最大队列长度typedef struct {QElemType *base; // 动态分配存储空间int front; // 头指针,若队列不空,// 指向队列头元素int rear; // 尾指针,若队列不空,指向// 队列尾元素 的下一个位置int queuesize;} SqQueue;循环队列——顺序映象图中阴影部分为队列中元素。如何判断一个循环队列是满还是空?Status InitQueue (SqQueue &Q,int maxsize) {// 构造一个最大存储空间为 maxsize 的// 空循环队列 QQ.base = new ElemType[maxsize];if (!Q.base) exit (OVERFLOW);Q.queuesize = maxsize;Q.front = Q.rear = 0;return OK;}Status EnQueue (SqQueue &Q, ElemType e) { // 插入元素e为Q的新的队尾元素if ((Q.rear+1) % Q.queuesize == Q.front)return ERROR; //队列满Q.base[Q.rear] = e;Q.rear = (Q.rear+1) % Q.queuesize;return OK;}Status DeQueue (SqQueue &Q, ElemType &e) { // 若队列不空,则删除Q的队头元素,// 用e返回其值,并返回OK; 否则返回ERRORif (Q.front == Q.rear) return ERROR;e = Q.base[Q.front];Q.front = (Q.front+1) % Q.queuesize;return OK;}队列应用示例一、计算 n 行杨辉三角的值二、“划分无冲突子集问题”求解第 1 行 1 1第 2 行 1 2 1第 3 行 1 3 3 1第 4 行 1 4 6 4 1二项式系数值(杨辉三角)设第 i-1行的值:(a[0]=0) a[1]..a[i] (a[i+1]=0)则第 i 行的值:b[j] = a[j-1]+a[j], j=1,2,…,i+1利用循环队列计算二项式的过程:假设只计算四行,则队列的最大容量为 5。1100q.frontq.rear11001q.frontq.rear11021q.frontq.rear11021q.frontq.rear10021q.frontq.rear10121q.frontq.rear10121q.frontq.rear10123q.frontq.rear10133q.frontq.rear10133q.frontq.reardo {DeQueue(Q, s);GetHead(Q, e);if (e!=0) printf (“%d”, e);EnQueue(Q, s+e);} while (e!=0);某运动会设立 N 个比赛项目,每个运动员可以参加一至三个项目。试问如何安排比赛日程既可以使同一运动员参加的项目不安排在同一单位时间进行,又使总的竞赛日程最短。若将此问题抽象成数学模型,则归属于“划分子集”问题。N 个比赛项目构成一个大小为 n 的集合,有同一运动员参加的项目则抽象为“冲突”关系。例如:某运动会设有 9 个项目:A = { 0,1,2,3,4,5,6,7,8 },七名运动员报名参加的项目分别为:(1,4,8)、(1,7)、(8,3)、(1,0,5)、(3,4)、(5,6,2)、(6,4)它们之间的冲突关系为: R ={(1,4),(4,8),(1,8),(1,7),(8,3),(1,0),(0,5),(1,5),(3,4),(5,6),(5,2),(6,2),(6,4)}将集合A划分成k个互不相交的子集A1,A2,…,Ak(k≤n),使同一子集中的元素均无冲突关系,并要求划分的子集数目尽可能地少。“划分子集”问题即为:对前述例子而言,问题即为:同一子集的项目为可以同时进行的项目,显然希望运动会的日程尽可能短。可利用“过筛”的方法来解决划分子集问题。从第一个元素考虑起,凡不和第一个元素发生冲突的元素都可以和它分在同一子集中,然后再“过筛”出一批互不冲突的元素为第二个子集,依次类推,直至所有元素都进入某个子集为止。 0123456780010001000110001101120000011003000010001401010010151110001006001011000701000000080101100000123456781456845880 1 0 0 0 1 0 0 00 1 0 0 0 2 1 0 00 1 0 0 1 2 1 0 10 2 0 0 1 2 1 0 1为了减少重复察看 R 数组的时间,可另设一个数组clash[n] 记录和当前已入组元素发生冲突的元素的信息。每次新开辟一组时,令clash 数组各分量的值均为“0”,当序号为“i”的元素入组时,将和该元素发生冲突的信息记入clash 数组。pre (前一个出队列的元素序号) = n; 组号 = 0;全体元素入队列;while ( 队列不空 ) {队头元素i出队列;if ( i < pre ) // 开辟新的组{ 组号++; clash 数组初始化; }if ( i 能入组 ) {i 入组,记下序号为 i 的元素所属组号;修改 clash 数组;}else i 重新入队列;pre = i;}划分子集算法的基本思想: 1. 掌握栈和队列类型的特点,并能在相应的应用问题中正确选用它们。 2. 熟练掌握栈类型的两种实现方法,特别应注意栈满和栈空的条件以及它们的描述方法。 3. 熟练掌握循环队列和链队列的基本操作实现算法,特别注意队满和队空的描述方法。4. 理解递归算法执行过程中栈的状态变化过程。QUICK QUIZ (3)一、基本知识题1. 什么是栈?什么是队列?它们各自的特点是什么?2. 线性表、栈、队列有什么异同?3. 简述栈的入栈、出栈操作的过程。4. 在循环队列中简述入队、出队操作的过程。二、算法设计题1. 设用一维数组stack[n]表示一个堆栈,若堆栈中一个元素需占用length个数组单元(length >1),试写出其入栈、出栈操作的算法。2. 试编写一个遍历及显示队列中元素的算法。3. 设一循环队列Queue,只有头指针front,不设尾指针,另设一个内含元素个数的计数器,试写出相应的入队、出队算法。4. 设计一算法能判断一个算术表达式中的圆括号配对是否正确。(提示:对表达式进行扫描,凡遇到“(”就进栈,遇到“)”就退出栈顶的“(”,表达式扫描完毕时栈若为空则圆括号配对正确。)返回习题解答(3)一、基本知识题答案1. 答:栈是限定在表的一端进行插入或删除操作的线性表;队列是元素的添加在表的一端进行,而元素的删除在表的另一端进行的线性表;栈的特点是后进先出,队列的特点是先进先出。2. 答:栈和队列都是线性表,但是是受限的线性表,对插入、删除运算加以限制。栈是只允许在一端进行插入、删除运算,因而是后进先出表;而队列是只允许在一端进行插入、另一端进行删除运算,因而是先进先出表。3. 答:栈的入栈、出栈操作均在栈顶进行,栈顶指针指向栈顶元素的下一个位置。入栈操作先将入栈元素放到栈顶指针所指示的位置上,然后将栈顶指针加1 。出栈操作先将栈顶指针减1,然后从栈顶指针指向位置取值。4. 答:在循环队列中,设队首指针指向队首元素,队尾指针指向队尾元素后的一个空闲元素。在队列不满时,可执行入队操作,此时先送值到队尾指针指向的空闲元素,队尾指针再加1(要取模)。在队列不空时,可执行出队操作,此时先从队首指针指向处取值,队首指针再加1(要取模)。二 、算法设计题答案1. 解:用一整型变量top表示栈顶指针,top为0时表示栈为空。如果栈不空,则从stack[1]开始存放元素。实现本题功能的函数如下:入栈算法:void Push(EleType x){if((top+length)>n)printf("上溢出\n");else{if(top==0) /*为空栈*/{top++;stack[top]=x;}else{top=top+length;stack[top]=x;}}}出栈算法:void Pop(EleType x){if(top==0)printf("为空栈\n");else{if(top==1){x=stack[top];top--;}else{x=stack[top];top=top-length;}}}2. 解:设表达式在字符数组a[ ]中,使用一堆栈S来帮助判断。实现本题功能的函数如下:int correct(char a[]){Stack S;InitStack(S);for(i=0;iif(a[i]=='(')Push(S,'(');else if (a[i]==')'){if(StackEmpty(S))return 0;elsePop(S);}if(StackEmpty(S))return 1; /*配对正确*/elsereturn 0; /*配对错误*/}3. 解:实现本题功能的函数如下:void travel(Queue, int front,rear){int i;for(i=front;i<=rear;i++){printf("%4d",Queue[i]);}}4. 解:用一个循环数组Queue[0,n-1]表示该循环队列,头指针为front,计数器count用来记录队列中结点的个数。入队算法如下:void enqueue(int x){int temp;if(count==n)printf("队列上溢出\n");else{count++;temp = (front+count)%n;Queue[temp]=x;}}出队算法如下:int dequeue(){int temp;if(count==0)printf("队列下溢出\n");else{temp=Queue[front];front=(front+1)%n;count--;return temp;}}返回(共102张PPT)5.1 数组的类型定义5.3 稀疏矩阵的压缩存储5.2 数组的顺序表示和实现5.4 广义表的类型定义5.5 广义表的表示方法5.6 广义表操作的递归函数5.1 数组的类型定义ADT Array {数据对象:D={aj1,j2, ...,,ji,jn| ji =0,...,bi -1, i=1,2,..,n }数据关系:R={R1, R2, ..., Rn}Ri={ | 0 jk bk -1,1 k n 且k i, 0 ji bi -2, i=2,...,n }} ADT Array基本操作:二维数组的定义:数据对象:D = {aij | 0≤i≤b1-1, 0 ≤j≤b2-1}数据关系:R = { ROW, COL }ROW = {| 0≤i≤b1-2, 0≤j≤b2-1}COL = {| 0≤i≤b1-1, 0≤ j≤b2-2}基本操作:InitArray(&A, n, bound1, ..., boundn)DestroyArray(&A)Value(A, &e, index1, ..., indexn)Assign(&A, e, index1, ..., indexn)InitArray(&A, n, bound1, ..., boundn)操作结果:若维数 n 和各维长度合法,则构造相应的数组A,并返回OK。DestroyArray(&A)操作结果:销毁数组A。Value(A, &e, index1, ..., indexn)初始条件:A是n维数组,e为元素变量,随后是n 个下标值。操作结果:若各下标不超界,则e赋值为所指定的A 的元素值,并返回OK。Assign(&A, e, index1, ..., indexn)初始条件:A是n维数组,e为元素变量,随后是n 个下标值。 操作结果:若下标不超界,则将e的值赋给所指定的A的元素,并返回OK。5.2 数组的顺序表示和实现类型特点:1) 只有引用型操作,没有加工型操作;2) 数组是多维的结构,而存储空间是一个一维的结构。有两种顺序映象的方式:1)以行序为主序;2)以列序为主序;例如:称为基地址或基址。以“行序为主序”的存储映象二维数组A中任一元素ai,j 的存储位置LOC(i,j) = LOC(0,0) + (b2×i+j)×a0,1a0,0a0,2a1,0a1,1a1,2a0,1a0,0a0,2a1,0a1,1a1,2LL推广到一般情况,可得到 n 维数组数据元素存储位置的映象关系称为 n 维数组的映象函数。数组元素的存储位置是其下标的线性函数其中 cn = L,ci-1 = bi ×ci , 1 < i n。LOC(j1, j2, ..., jn ) = LOC(0,0,...,0) + ∑ ci jii=1n假设 m 行 n 列的矩阵含 t 个非零元素,则称为稀疏因子通常认为 0.05 的矩阵为稀疏矩阵5.3 稀疏矩阵的压缩存储何谓稀疏矩阵?以常规方法,即以二维数组表示高阶的稀疏矩阵时产生的问题:1) 零值元素占了很大空间;2) 计算中进行了很多和零值的运算,遇除法,还需判别除数是否为零;1) 尽可能少存或不存零值元素;解决问题的原则:2) 尽可能减少没有实际意义的运算;3) 操作方便; 即:能尽可能快地找到与下标值 (i, j) 对应的元素;能尽可能快地找到同一行或同一列的非零值元;1) 特殊矩阵非零元在矩阵中的分布有一定规则例如: 三角矩阵对角矩阵2) 随机稀疏矩阵非零元在矩阵中随机出现有两类稀疏矩阵:随机稀疏矩阵的压缩存储方法:一、三元组顺序表二、行逻辑联接的顺序表三、 十字链表#define MAXSIZE 12500typedef struct {int i, j; //该非零元的行下标和列下标ElemType e; // 该非零元的值} Triple; // 三元组类型一、三元组顺序表typedef union {Triple data[MAXSIZE + 1];int mu, nu, tu;} TSMatrix; // 稀疏矩阵类型如何求转置矩阵?用常规的二维数组表示时的算法其时间复杂度为: O(mu×nu)for (col=1; col<=nu; ++col)for (row=1; row<=mu; ++row)T[col][row] = M[row][col];用“三元组”表示时如何实现?1 2 141 5 -52 2 -73 1 363 4 282 1 145 1 -52 2 -71 3 364 3 28首先应该确定转置矩阵中每一行的第一个非零元在三元组中的位置。cpot[1] = 1;for (col=2; col<=M.nu; ++col)cpot[col] = cpot[col-1] + num[col-1];Status FastTransposeSMatrix(TSMatrix M, TSMatrix &T){T.mu = M.nu; T.nu = M.mu; T.tu = M.tu;if (T.tu) {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;for (col=2; col<=M.nu; ++col)cpot[col] = cpot[col-1] + num[col-1];for (p=1; p<=M.tu; ++p) { }} // ifreturn OK;} // FastTransposeSMatrix转置矩阵元素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]分析算法FastTransposeSMatrix的时间复杂度:时间复杂度为: O(M.nu+M.tu)for (col=1; col<=M.nu; ++col) … …for (t=1; t<=M.tu; ++t) … …for (col=2; col<=M.nu; ++col) … …for (p=1; p<=M.tu; ++p) … …三元组顺序表又称有序的双下标法,它的特点是,非零元在表中按行序有序存储,因此便于进行依行顺序处理的矩阵运算。然而,若需随机存取某一行中的非零元,则需从头开始进行查找。二、行逻辑联接的顺序表#define MAXMN 500typedef struct {Triple data[MAXSIZE + 1];int rpos[MAXMN + 1];int mu, nu, tu;} RLSMatrix; // 行逻辑链接顺序表类型修改前述的稀疏矩阵的结构定义,增加一个数据成员rpos, 其值在稀疏矩阵的初始化函数中确定。例如:给定一组下标,求矩阵的元素值ElemType value(RLSMatrix M, int r, int c) {p = M.rpos[r];while (M.data[p].i==r &&M.data[p].j < c)p++;if (M.data[p].i==r && M.data[p].j==c)return M.data[p].e;else return 0;} // value矩阵乘法的精典算法: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];}其时间复杂度为: O(m1×n2×n1)Q初始化;if Q是非零矩阵 { // 逐行求积for (arow=1; arow<=M.mu; ++arow) {// 处理M的每一行ctemp[] = 0; // 累加器清零计算Q中第arow行的积并存入ctemp[] 中;将ctemp[] 中非零元压缩存储到Q.data;} // for arow} // if两个稀疏矩阵相乘(Q M N)的过程可大致描述如下:Status MultSMatrix(RLSMatrix M, RLSMatrix N, RLSMatrix &Q) {if (M.nu != N.mu) return ERROR;Q.mu = M.mu; Q.nu = N.nu; Q.tu = 0;if (M.tu*N.tu != 0) { // Q是非零矩阵for (arow=1; arow<=M.mu; ++arow) {// 处理M的每一行} // for arow} // ifreturn OK;} // MultSMatrixctemp[] = 0; // 当前行各元素累加器清零Q.rpos[arow] = Q.tu+1;for (p=M.rpos[arow]; p//对当前行中每一个非零元brow=M.data[p].j;if (brow < N.nu ) t = N.rpos[brow+1];else { t = N.tu+1 }for (q=N.rpos[brow]; q< t; ++q) {ccol = N.data[q].j; // 乘积元素在Q中列号ctemp[ccol] += M.data[p].e * N.data[q].e;} // for q} // 求得Q中第crow( =arow)行的非零元for (ccol=1; ccol<=Q.nu; ++ccol) if (ctemp[ccol]) {if (++Q.tu > MAXSIZE) return ERROR;Q.data[Q.tu] = {arow, ccol, ctemp[ccol]};} // if处理 的每一行M分析上述算法的时间复杂度累加器ctemp初始化的时间复杂度为 (M.mu N.nu),求Q的所有非零元的时间复杂度为 (M.tu N.tu/N.mu),进行压缩存储的时间复杂度为 (M.mu N.nu),总的时间复杂度就是 (M.mu N.nu+M.tu N.tu/N.mu)。若M是m行n列的稀疏矩阵,N是n行p列的稀疏矩阵,则M中非零元的个数 M.tu = M m n,N中非零元的个数 N.tu = N n p,相乘算法的时间复杂度就是 (m p (1+n M N)) ,当 M<0.05 和 N<0.05及 n <1000时,相乘算法的时间复杂度就相当于 (m p)。三、 十字链表3 0 0 50 -1 0 02 0 0 011314522-1312^^^^^^^5.4 广义表的类型定义ADT Glist {数据对象:D={ei | i=1,2,..,n; n≥0;ei∈AtomSet 或 ei∈GList,AtomSet为某个数据对象 }数据关系:LR={| ei-1 ,ei∈D, 2≤i≤n}} ADT Glist基本操作:广义表是递归定义的线性结构,LS = ( 1, 2, , n )其中: i 或为原子 或为广义表例如: A = ( )F = (d, (e))D = ((a,(b,c)), F)C = (A, D, F)B = (a, B) = (a, (a, (a, , ) ) )广义表是一个多层次的线性结构例如:D=(E, F)其中:E=(a, (b, c))F=(d, (e))DEFa( )d( )bce广义表 LS = ( 1, 2, …, n )的结构特点:1) 广义表中的数据元素有相对次序;2) 广义表的长度定义为最外层包含元素个数;3) 广义表的深度定义为所含括弧的重数;注意:“原子”的深度为 0 ;“空表”的深度为 1 。4) 广义表可以共享;5) 广义表可以是一个递归的表;递归表的深度是无穷值,长度是有限值。6) 任何一个非空广义表 LS = ( 1, 2, …, n)均可分解为表头 Head(LS) = 1 和表尾 Tail(LS) = ( 2, …, n) 两部分例如: D = ( E, F ) = ((a, (b, c)),F )Head( D ) = E Tail( D ) = ( F )Head( E ) = a Tail( E ) = ( ( b, c) )Head( (( b, c)) ) = ( b, c) Tail( (( b, c)) ) = ( )Head( ( b, c) ) = b Tail( ( b, c) ) = ( c )Head( ( c ) ) = c Tail( ( c ) ) = ( ) 结构的创建和销毁InitGList(&L); DestroyGList(&L);CreateGList(&L, S); CopyGList(&T, L);基本操作 状态函数GListLength(L); GListDepth(L);GListEmpty(L); GetHead(L); GetTail(L); 插入和删除操作InsertFirst_GL(&L, e);DeleteFirst_GL(&L, &e); 遍历Traverse_GL(L, Visit());5.5 广义表的表示方法通常采用头、尾指针的链表结构表结点:原子结点:tag=1 hp tptag=0 data1) 表头、表尾分析法:构造存储结构的两种分析方法:若表头为原子,则为空表 ls=NIL非空表 lstag=1指向表头的指针指向表尾的指针tag=0 data否则,依次类推。L=(a, (x, y), ((x)))L ((x, y), ((x))) (((x)))1 1a (x, y)L = ( a, ( x, y ), ( ( x ) ) )a( x, y )( )1LL = ( )0 a111110 x ( )x2) 子表分析法:若子表为原子,则为空表 ls=NIL非空表 ls=( 1, 2, …, n)1指向子表a1的指针tag=0 data否则,依次类推。1指向子表a2的指针1指向子表an的指针ls… 例如: a (x, y) ((x))LS=( a, (x,y), ((x)) )5.6 广义表操作的递归函数递归函数一个含直接或间接调用本函数语句的函数被称之为递归函数,它必须满足以下两个条件:1)在每一次调用自己时,必须是(在某种意义上)更接近于解;2)必须有一个终止处理或计算的准则。一. 有如下递归过程:Void pf(int w){ int i;if (w!=0){ pf(w-1);for ( i=1; i<=w ; i++) printf(“%3d”,w);printf(“\n”);}}调用语句pf(4)的结果是:123 34 4 4 4二. 写出下列递归过程的结果:Void revers( ){ char ch;scanf(“%c”,&ch);if ( ch!=“.”){revers;printf( “%c”,ch);}}读入任意长度的字符串,以“.”结束,打印出它们的逆序字符串一、分治法 (Divide and Conquer)(又称分割求解法)如何设计递归函数?二、后置递归法(Postponing the work)三、回溯法(Backtracking)对于一个输入规模为 n 的函数或问题,用某种方法把输入分割成 k(1从而产生 l 个子问题,分别求解这 l 个问题,得出 l 个问题的子解,再用某种方法把它们组合成原来问题的解。若子问题还相当大,则可以反复使用分治法,直至最后所分得的子问题足够小,以至可以直接求解为止。分治法的设计思想为:在利用分治法求解时,所得子问题的类型常常和原问题相同,因而很自然地导致递归求解。例如:焚塔问题: Hanoi(n, x, y, z)可递归求解 Hanoi(n-1, x, z, y)将 n 个盘分成两个子集(1至n-1 和 n ),从而产生下列三个子问题:1) 将1至n-1号盘从 x 轴移动至 y 轴;3) 将1至n-1号盘从y轴移动至z轴;2) 将 n号盘从 x 轴移动至 z 轴Move( n, ‘X’ ‘Z’)可递归求解 Hanoi(n-1, y, x, z)n=1时可以直接求解例如: 梵塔的递归函数void hanoi (int n, char x, char y, char z){if (n==1)move(x, 1, z);else {hanoi(n-1, x, z, y);move(x, n, z);hanoi(n-1, y, x, z);}}广义表从结构上可以分解成广义表 = 表头 + 表尾或者广义表 =子表1 + 子表2 + ··· + 子表n因此常利用分治法求解之。算法设计中的关键问题是,如何将 l 个子问题的解组合成原问题的解。广义表的头尾链表存储表示:typedef enum {ATOM, LIST} ElemTag;// ATOM==0:原子, LIST==1:子表typedef struct GLNode {ElemTag tag; // 标志域union{AtomType atom; // 原子结点的数据域struct {struct GLNode *hp, *tp;} ptr;};} *GListtag=1hp tpptr表结点2) 子表分析法:若子表为原子,则为空表 ls=NIL非空表 ls=( 1, 2, …, n)1指向子表a1的指针tag=0 data否则,依次类推。1指向子表a2的指针1指向子表an的指针ls… 广义表的深度=Max {子表的深度} +1例如: 求广义表的深度可以直接求解的两种简单情况为:空表的深度 = 1原子的深度 = 0将广义表分解成 n 个子表,分别(递归)求得每个子表的深度,int GlistDepth(Glist L) {// 返回指针L所指的广义表的深度for (max=0, pp=L; pp; pp=pp->ptr.tp){dep = GlistDepth(pp->ptr.hp);if (dep > max) max = dep;}return max + 1;} // GlistDepthif (!L) return 1;if (L->tag == ATOM) return 0;111L… for (max=0, pp=L; pp; pp=pp->ptr.tp){dep = GlistDepth(pp->ptr.hp);if (dep > max) max = dep;}例如:pppp->ptr.hppppppp->ptr.hppp->ptr.hpL = ( a, ( x, y ), ( ( x ) ) )a( x, y )( )1LL = ( )0 a111110 x ( )x后置递归的设计思想为:递归的终结状态是,当前的问题可以直接求解,对原问题而言,则是已走到了求解的最后一步。链表是可以如此求解的一个典型例子。例如:编写“删除单链表中所有值为x 的数据元素”的算法。1) 单链表是一种顺序结构,必须从第一个结点起,逐个检查每个结点的数据元素;分析:2) 从另一角度看,链表又是一个递归结构,若 L 是线性链表 (a1, a2, , an) 的头指针,则 L->next是线性链表 (a2, , an)的头指针。a1a2a3an… L例如:a1a2a3an La1a2a3an L已知下列链表1) “a1=x”, 则 L 仍为删除 x 后的链表头指针2) “a1≠x”, 则余下问题是考虑以 L->next 为头指针的链表……a1L->nextL->next=p->nextp=L->nextvoid delete(LinkList &L, ElemType x) {// 删除以L为头指针的带头结点的单链表中// 所有值为x的数据元素if (L->next) {if (L->next->data==x) {p=L->next; L->next=p->next;free(p); delete(L, x);}else delete(L->next, x);}} // delete删除广义表中所有元素为x的原子结点分析:比较广义表和线性表的结构特点:相似处:都是链表结构。不同处:1)广义表的数据元素可能还是个广义表;2)删除时,不仅要删除原子结点,还需要删除相应的表结点。void Delete_GL(Glist&L, AtomType x) {//删除广义表L中所有值为x的原子结点if (L) {head = L->ptr.hp; // 考察第一个子表if ((head->tag == Atom) &&(head->atom == x)){ } // 删除原子项 x的情况else{ }// 第一项没有被删除的情况}} // Delete_GL… …… …p=L; L = L->ptr.tp; // 修改指针free(head); // 释放原子结点free(p); // 释放表结点Delete_GL(L, x); // 递归处理剩余表项1L0 x1pLheadif (head->tag == LIST) //该项为广义表Delete_GL(head, x);Delete_GL(L->ptr.tp, x);// 递归处理剩余表项1L0 a11headL->ptr.tp综合几点:1. 对于含有递归特性的问题,最好设计递归形式的算法。但也不要单纯追求形式,应在算法设计的分析过程中“就事论事”。例如,在利用分割求解设计算法时,子问题和原问题的性质相同;或者,问题的当前一步解决之后,余下的问题和原问题性质相同,则自然导致递归求解。2. 实现递归函数,目前必须利用“栈”。一个递归函数必定能改写为利用栈实现的非递归函数;反之,一个用栈实现的非递归函数可以改写为递归函数。需要注意的是递归函数递归层次的深度决定所需存储量的大小。3. 分析递归算法的工具是递归树,从递归树上可以得到递归函数的各种相关信息。例如:递归树的深度即为递归函数的递归深度;递归树上的结点数目恰为函数中的主要操作重复进行的次数;若递归树蜕化为单支树或者递归树中含有很多相同的结点,则表明该递归函数不适用。例如: n=3的梵塔算法中主要操作move的执行次数可以利用下列递归树进行分析:move(3, a, b, c)move(2, a, c, b)move(2, b, a, c)move(1, a, b, c)move(1, c, a, b)move(1, b, c, a)move(1, a, b, c)上图递归树的中序序列即为圆盘的移动操作序列。又如: 求n!的递归函数的递归树已退化为一个单枝树;而计算斐波那契递归函数的递归树中有很多重复出现的结点。nn-110。。。F5F4F3F3F2F2F1F1F0F1F0。。。如: void delete(LinkList &L, ElemType x) {// L为无头结点的单链表的头指针if (L) {if (L->data=x) {p=L; L=L->next;free(p); delete(L, x);}else delete(L->next, x);}}4. 递归函数中的尾递归容易消除。void delete(LinkList &L, ElemType x) {// L为带头结点的单链表的头指针p=L->next; pre=L;while (p) {if (p->data=x) {pre->next=p->next;free(p); p=pre->next;}else { pre=p; p=p->next; }}}可改写为1. 了解数组的两种存储表示方法,并掌握数组在以行为主的存储结构中的地址计算方法。2. 掌握对特殊矩阵进行压缩存储时的下标变换公式。3. 了解稀疏矩阵的两类压缩存储方法的特点和适用范围,领会以三元组表示稀疏矩阵时进行矩阵运算采用的处理方法。4. 掌握广义表的结构特点及其存储表示方法,读者可根据自己的习惯熟练掌握任意一种结构的链表,学会对非空广义表进行分解的两种分析方法:即可将一个非空广义表分解为表头和表尾两部分或者分解为n个子表。5. 学习利用分治法的算法设计思想编制递归算法的方法。QUICK QUIZ(5)1. A[10][20]采用列为主序存储,每个元素占1个单元,A[0][0]的地址为200,则A[6][12]的地址是多少 2. 稀疏矩阵m×n采用三元组顺序表存储结构,非零元个数tu满足什么条件时,该存储结构才有意义 3. 二维数组A[10..20][5..10]采用行为主序存储,每个元素占4个单元,A[10][5]的地址为1000,则A[18][9]的地址是多少 4. 稀疏矩阵的三元组顺序表存储结构是随机存储结构吗 为什么 5. 广义表((a,b,c,d))的表头和表尾分别是什么 6. 广义表((a),((b),c),(((d))))的长度和深度分别是多少 (326)(tu< m×n/3)(1208)(不是)((a,b,c,d),空表)(3, 4)又如:遍历二叉树: Traverse(BT)可递归求解 Traverse(LBT)将 n 个结点分成三个子集(根结点、左子树 和右子树 ),从而产生下列三个子问题:1) 访问根结点;3) 遍历右子树;2) 遍历左子树;可递归求解 Traverse(RBT)二叉树的遍历void PreOrderTraverse( BiTree T,void (Visit)(BiTree P)){if (T) {Visit(T->data);(PreOrderTraverse(T->lchild, Visit);(PreOrderTraverse(T->rchild, Visit);}} // PreOrderTraverse例一 求广义表的深度例二 复制广义表例三 创建广义表的存储结构例二 复制广义表新的广义表由新的表头和表尾构成。可以直接求解的两种简单情况为:空表复制求得的新表自然也是空表;原子结点可以直接复制求得。将广义表分解成表头和表尾两部分,分别(递归)复制求得新的表头和表尾,若 ls= NIL 则 newls = NIL否则构造结点 newls,由 表头ls->ptr.hp 复制得 newhp由 表尾 ls->ptr.tp 复制得 newtp并使 newls->ptr.hp = newhp,newls->ptr.tp = newtp复制求广义表的算法描述如下:Status CopyGList(Glist &T, Glist L) {if (!L) T = NULL; // 复制空表else {if ( !(T = (Glist)malloc(sizeof(GLNode))) )exit(OVERFLOW); // 建表结点T->tag = L->tag;if (L->tag == ATOM)T->atom = L->atom; // 复制单原子结点else { }} // elsereturn OK;} // CopyGList分别复制表头和表尾CopyGList(T->ptr.hp, L->ptr.hp);// 复制求得表头T->ptr.hp的一个副本L->ptr.hpCopyGList(T->ptr.tp, L->ptr.tp);// 复制求得表尾T->ptr.tp 的一个副本L->ptr.tp语句 CopyGList(T->ptr.hp, L->ptr.hp);等价于CopyGList(newhp, L->ptr.tp);T->ptr.hp = newhp;例三 创建广义表的存储结构对应广义表的不同定义方法相应地有不同的创建存储结构的算法。假设以字符串 S = ( 1, 2, , n ) 的形式定义广义表 L,建立相应的存储结构。由于S中的每个子串 i定义 L 的一个子表,从而产生 n 个子问题,即分别由这 n个子串 (递归)建立 n 个子表,再组合成一个广义表。可以直接求解的两种简单情况为:由串 ( ) 建立的广义表是空表;由单字符建立的子表只是一个原子结点。若 S = ( ) 则 L = NIL否则 构造第一个表结点 *L,并从串S中分解出第一个子串 1, 对应 创建第一个子广义表 L->ptr.hp;若剩余串非空,则构造第二个表结点 L->ptr.tp, 并从串S中分解出第二个子串 2, 对应建第二个子广义表 ………;依次类推,直至剩余串为空串止。void CreateGList(Glist &L, String S) {if (空串) L = NULL; // 创建空表else {L=(Glist) malloc(sizeof(GLNode));L->tag=List; p=L;sub=SubString(S,2,StrLength(S)-1);//脱去串S的外层括弧} // else}由sub中所含n个子串建立n个子表;do {sever(sub, hsub); // 分离出子表串hsub= iif (!StrEmpty(sub) {p->ptr.tp=(Glist)malloc(sizeof(GLNode));// 建下一个子表的表结点*(p->ptr.tp)p=p->ptr.tp;}} while (!StrEmpty(sub));p->ptr.tp = NULL; // 表尾为空表创建由串hsub定义的广义表p->ptr.hp;if (StrLength(hsub)==1) {p->ptr.hp=(GList)malloc(sizeof(GLNode));p->ptr.hp->tag=ATOM;p->ptr.hp->atom=hsub; // 创建单原子结点}else CreateGList(p->ptr.hp, hsub);//递归建广义表4. 递归函数中的尾递归容易消除。例如:先序遍历二叉树可以改写为:void PreOrderTraverse( BiTree T) {While (T) {Visit(T->data);PreOrderTraverse(T->lchild);T = T->rchild;}} // PreOrderTraverse回溯法是一种“穷举”方法。其基本思想为:假设问题的解为 n 元组 (x1, x2, …, xn),其中 xi 取值于集合 Si。n 元组的子组 (x1, x2, …, xi) (i对于已求得的部分解 (x1, x2, …, xi) ,若在添加 xi+1 Si+1 之后仍然满足约束条件,则得到一个新的部分解 (x1, x2, …, xi+1) ,之后继续添加 xi+2 Si+2 并检查之;例一、皇后问题求解设四皇后问题的解为 (x1, x2, x3, x4),其中: xi (i=1,2,3,4) Si={1, 2, 3, 4}约束条件为: 其中任意两个xi 和xj不能位于棋盘的同行、同列及同对角线。按回溯法的定义,皇后问题求解过程为:解的初始值为空;首先添加 x1=1, 之后添加满足条件的 x2=3,由于对所有的 x3 {1,2, 3, 4}都不能找到满足约束条件的部分解(x1, x2, x3), 则回溯到部分解(x1), 重新添加满足约束条件的x2=4, 依次类推。void Trial(int i, int n) {// 进入本函数时,在n×n棋盘前i-1行已放置了互不攻// 击的i-1个棋子。现从第 i 行起继续为后续棋子选择// 满足约束条件的位置。当求得(i>n)的一个合法布局// 时,输出之。if (i>n) 输出棋盘的当前布局;else for (j=1; j<=n; ++j) {在第 i 行第 j 列放置一个棋子;if (当前布局合法) Trial(i+1, n);移去第 i 行第 j 列的棋子;}} // trial回溯法求解的算法一般形式:void B(int i, int n) {// 假设已求得满足约束条件的部分解(x1,..., xi-1),本函//数从 xi 起继续搜索,直到求得整个解(x1, x2, … xn)。if (i>n)else while ( ! Empty(Si)) {从 Si 中取 xi 的一个值 vi Si;if (x1, x2, …, xi) 满足约束条件B( i+1, n); // 继续求下一个部分解从 Si 中删除值 vi;}} // B5. 可以用递归方程来表述递归函数的时间性能。例如: 假设解n个圆盘的梵塔的执行时间为T(n)则递归方程为: T(n) = 2T(n-1) + C,初始条件为: T(0) = 0数据结构--线性表初始化操作LnitList(&L)构造一个空的线性表L结构销毁操作初始条件:线性表L已经存在DestroyList(&L)销毁线性表L引用型操作:ListEmpty(L)(线性表判空)若L为空,返回为TRUE,否则返回为FALSE。ListLength(L)(求线性表的长度)返回L中的元素个数PriorElem(L, cur_e, &pre_e)(求数据元素的的前驱)若cur_e是L的元素,但不是第一个,则用pre_e返回它的前驱,否则操作失败,pre_e无定义。NextElem(L, cur_e, &next_e) (求数据元素的后继)若cur_e是L的元素,但不是最后一个,则用next_e返回它的后继,否则操作失败,next_e无定义。GetElem(L, i, &e) (求线性表中某个数据元素)用e返回L中的第i个元素的值。LocateElem(L ,e, compare( ) ) (定位函数)线性表已经存在,e为给定值,compare()是元素判定函数。返回L中第1个与e满足关系compare( )的元素的位序。若这样的元素不存在则返回值为0ListTraverse(L, visit( )) (遍历线性表)Visit() 为某个访问函数。依次对L的每个元素调用函数visit( )。一旦visit( )失败,则操作失败。ClearList( &L )线性表置空加工型操作11.PutElem( &L, i, &e )(改变数据元素的值)1≤i≤LengthList(L)L中第i个元素赋值同e的值(共77张PPT)4.1 串的抽象数据类型的定义4.2 串的表示和实现4.3 串的模式匹配算法4.1 串的抽象数据类型的定义如下:ADT String {数据对象:D={ ai |ai∈CharacterSet,i=1,2,...,n, n≥0 }数据关系:R1={ < ai-1, ai > | ai-1, ai ∈D,i=2,...,n }串是有限长的字符序列,由一对双引号相括,如: a string 基本操作:StrAssign (&T, chars)StrCopy (&T, S)DestroyString(&S)StrEmpty (S)StrCompare (S, T)StrLength(S)Concat (&T, S1, S2)SubString (&Sub, S, pos, len)Index (S, T, pos)Replace (&S, T, V)StrInsert (&S, pos, T)StrDelete (&S, pos, len)ClearString (&S)} ADT StringStrAssign (&T, chars)初始条件:chars 是字符串常量。操作结果:把 chars 赋为 T 的值。StrCopy (&T, S)初始条件:串 S 存在。操作结果:由串 S 复制得串 T。DestroyString (&S)初始条件:串 S 存在。操作结果:串 S 被销毁。StrEmpty (S)初始条件:串S存在。操作结果:若 S 为空串,则返回 true,否则返回 false。 表示空串,空串的长度为零。StrCompare (S, T)初始条件:串 S 和 T 存在。操作结果:若S T,则返回值 0;若S T,则返回值 0;若S T,则返回值 0。例如:StrCompare( data , state ) < 0StrCompare( cat , case ) > 0StrLength (S)初始条件:串 S 存在。操作结果:返回 S 的元素个数,称为串的长度。Concat (&T, S1, S2)初始条件:串 S1 和 S2 存在。操作结果:用 T 返回由 S1 和 S2联接而成的新串。例如: Concate( T, man , kind )求得 T = mankind SubString (&Sub, S, pos, len)初始条件:操作结果:用 Sub 返回串 S 的第 pos 个字符起长度为 len 的子串。串 S 存在,1≤pos≤StrLength(S) 且 0≤len≤StrLength(S)-pos+1。例如:SubString( sub, commander , 4, 3)子串为“串”中的一个字符子序列求得 sub = man ;SubString( sub, commander , 1, 9)SubString( sub, commander , 9, 1)求得 sub = r 求得 sub = commander SubString(sub, commander , 4, 7)sub = SubString(sub, beijing , 7, 2) = sub = SubString( student , 5, 0) = 起始位置和子串长度之间存在约束关系长度为 0 的子串为“合法”串Index (S, T, pos)初始条件:串S和T存在,T是非空串,1≤pos≤StrLength(S)。操作结果: 若主串 S 中存在和串 T 值相同的子串, 则返回它在主串 S 中第pos个字符之后第一次出现的位置;否则函数值为0。假设 S = abcaabcaaabc , T = bca Index(S, T, 1) = 2;Index(S, T, 3) = 6;Index(S, T, 8) = 0;“子串在主串中的位置”意指子串中的第一个字符在主串中的位序。Replace (&S, T, V)初始条件:串S, T和 V 均已存在,且 T 是非空串。操作结果:用 V 替换主串 S 中出现的所有与(模式串)T相等的不重叠的子串。例如:假设 S = abcaabcaaabca , T = bca 若 V = x , 则经置换后得到S = axaxaax 若 V = bc , 则经置换后得到S = abcabcaabc bcabcabcaStrInsert (&S, pos, T) 初始条件:串S和T存在,1≤pos≤StrLength(S)+1。 操作结果:在串S的第pos个字符之前插入串T。例如:S = chater ,T = rac ,则执行 StrInsert(S, 4, T) 之后得到S = character StrDelete (&S, pos, len) 初始条件:串S存在1≤pos≤StrLength(S)-len+1。 操作结果:从串S中删除第pos个字符 起长度为len的子串。ClearString (&S)初始条件:串S存在。操作结果:将S清为空串。对于串的基本操作集可以有不同的定义方法,在使用高级程序设计语言中的串类型时,应以该语言的参考手册为准。gets(str) 输入一个串;puts(str) 输出一个串;strcat(str1, str2) 串联接函数;strcpy(str1, str2, k) 串复制函数;strcmp(str1, str2) 串比较函数;strlen(str) 求串长函数;例如:C语言函数库中提供下列串处理函数:串赋值StrAssign、串复制Strcopy、串比较StrCompare、求串长StrLength、串联接Concat以及求子串SubString等六种操作构成串类型的最小操作子集。在上述抽象数据类型定义的13种操作中,即:这些操作不可能利用其他串操作来实现,反之,其他串操作(除串清除ClearString和串销毁DestroyString外)可在这个最小操作子集上实现。例如,可利用串比较、求串长和求子串等操作实现定位函数 Index( S, T, pos )。StrCompare(SubString(S, i, StrLength(T)), T )S 串T 串T 串iposn-m+1算法的基本思想为: 0int Index (String S, String T, int pos) {// T为非空串。若主串S中第pos个字符之后存在与 T相等的子串,// 则返回第一个这样的子串在S中的 位置,否则返回0if (pos > 0) {} // ifreturn 0; // S中不存在与T相等的子串} // Indexn = StrLength(S); m = StrLength(T); i = pos;while ( i <= n-m+1) {} // whileSubString (sub, S, i, m);if (StrCompare(sub,T) != 0) ++i ;else return i ;又如串的置换函数:S 串T 串V 串V 串pospossubinews 串sub= i+mposn-pos+1void replace(String& S, String T, String V) {}while ( pos <= n-m+1 && i) {i=Index(S, T, pos);if (i!=0) {SubString(sub, S, pos, i-pos); // 不置换子串Concat(news, news, sub, V);pos = i+m;}//if}//whileSubString(sub, S, pos, n-pos+1); // 剩余串Concat( S, news, sub );n=StrLength(S); m=StrLength(T); pos = 1;StrAssign(news, NullStr); i=1;串的逻辑结构和线性表极为相似,区别仅在于串的数据对象约束为字符集。串的基本操作和线性表有很大差别。在线性表的基本操作中,大多以“单个元素”作为操作对象;在串的基本操作中,通常以“串的整体”作为操作对象。在程序设计语言中,串只是作为输入或输出的常量出现,则只需存储此串的串值,即字符序列即可。但在多数非数值处理的程序中,串也以变量的形式出现。4.2 串的表示和实现一、串的定长顺序存储表示二、串的堆分配存储表示三、串的块链存储表示#define MAXSTRLEN 255// 用户可在255以内定义最大串长typedef unsigned char Sstring[MAXSTRLEN + 1];// 0号单元存放串的长度一、串的定长顺序存储表示按这种串的表示方法实现的串的运算时,其基本操作为 “字符序列的复制”串的实际长度可在这个予定义长度的范围内随意设定,超过予定义长度的串值则被舍去,称之为“截断”特点:Status Concat(SString S1, SString S2, SString &T) {// 用T返回由S1和S2联接而成的新串。若未截断, 则返回TRUE,否则FALSE。return uncut;} // Concat例如:串的联接算法中需分三种情况处理:T[1..S1[0]] = S1[1..S1[0]];T[S1[0]+1..S1[0]+S2[0]] = S2[1..S2[0]];T[0] = S1[0]+S2[0]; uncut = TRUE; }if (S1[0]+S2[0] <= MAXSTRLEN) {// 未截断else if (S1[0] else { // 截断(仅取S1)T[1..S1[0]] = S1[1..S1[0]];T[S1[0]+1..MAXSTRLEN] =S2[1..MAXSTRLEN-S1[0]];T[0] = MAXSTRLEN; uncut = FALSE; }T[0..MAXSTRLEN] = S1[0..MAXSTRLEN];// T[0] == S1[0] == MAXSTRLENuncut = FALSE; }typedef struct {char *ch;// 若是非空串,则按串实用长度分配//存储区,否则 ch 为NULLint length; // 串长度} HString;二、串的堆分配存储表示通常,C语言中提供的串类型就是以这种存储方式实现的。系统利用函数malloc( ) 和 free( ) 进行串值空间的动态管理,为每一个新产生的串分配一个存储区,称串值共享的存储空间为“堆”。C语言中的串以一个空字符为结束符,串长是一个隐含值。这类串操作实现的算法为:先为新生成的串分配一个存储空间,然后进行串值的复制。Status Concat(HString &T, HString S1, HString S2) {// 用T返回由S1和S2联接而成的新串if (T.ch) delete(T.ch); // 释放旧空间if (!(T.ch =new char[S1.length+S2.length]))exit (OVERFLOW);T.ch[0..S1.length-1] = S1.ch[0..S1.length-1];T.length = S1.length + S2.length;T.ch[S1.length..T.length-1] = S2.ch[0..S2.length-1];return OK;} // ConcatStatus SubString(HString &Sub, HString S,int pos, int len) {// 用Sub返回串S的第pos个字符起长度为len的子串if (pos < 1 || pos > S.length|| len < 0 || len > S.length-pos+1)return ERROR;if (Sub.ch) delete (Sub.ch); // 释放旧空间if (!len){ Sub.ch = NULL; Sub.length = 0; } // 空子串else { } // 完整子串return OK;} // SubString… …if(!(Sub.ch = new char[len]))return ERROR;Sub.ch[0..len-1] = S.ch[pos-1..pos+len-2];Sub.length = len;void StrInsert (Hstring& S, int pos, HString T) {// 1≤pos≤StrLength(S)+1。在串 S 的// 第 pos 个字符之前插入串 Tslen=S.length; tlen=T.length; // 取得S和T的串长if (pos < 1 || pos > slen+1) return; // 插入位置不合法S1.ch = new char[slen] ; // S1作为辅助串S1.ch[0..slen-1] = S.ch[0..slen-1]; // 暂存 Sif (tlen>0) // T 非空,则为S重新分配空间并插入T{ }} // StrInsert _HSq……S.ch = new char[slen + tlen ];// 为 S 重新分配空间for ( i=0, k=0; i// 保留插入位置之前的子串for ( i=0; i// 插入Tfor ( i=pos; i// 复制插入位置之后的子串S.length = slen+tlen;delete S1.ch;三、串的块链存储表示也可用链表来存储串值,由于串的数据元素是一个字符,它只有 8 位二进制数,因此用链表存储时,通常一个结点中存放的不是一个字符,而是一个子串。存储密度 =数据元素所占存储位实际分配的存储位#define CHUNKSIZE 80 // 可由用户定义的块大小typedef struct Chunk { // 结点结构char ch[CUNKSIZE];struct Chunk *next;} Chunk;typedef struct { // 串的链表结构Chunk *head, *tail; // 串的头和尾指针int curlen; // 串的当前长度} LString;例如: 在编辑系统中,整个文本编辑区可以看成是一个串,每一行是一个子串,构成一个结点。即: 同一行的串用定长结构(80个字符), 行和行之间用指针相联接。实际应用时,可以根据问题所需来设置结点的大小。这是串的一种重要操作,很多软件,若有“编辑”菜单项的话,则其中必有“查找”子菜单项。4.3 串的模式匹配算法初始条件:串 S 和 T 存在,T 是非空串,1≤pos≤StrLength(S)。首先,回忆一下串匹配(查找)的定义:INDEX (S, T, pos)操作结果:若主串 S 中存在和串 T 值相同的子串返回它在主串 S 中第 pos 个字符之后第一次出现的位置; 否则函数值为0。int Index (String S, String T, int pos) {// T为非空串。若主串S中第pos个字符之后存在与 T相等的子串,则返回第一个这样的子串在S中的 位置,否则返回0if (pos > 0) {n = StrLength(S); m = StrLength(T); i = pos;while ( i <= n-m+1) {SubString (sub, S, i, m);if (StrCompare(sub,T) != 0) ++i ;else return i ;} // while} // ifreturn 0; // S中不存在与T相等的子串} // Index下面讨论以定长顺序结构表示串时的几种算法。一、简单算法三、KMP(D.E.Knuth,V.R.Pratt,J.H.Morris) 算法二、首尾匹配算法S 串T 串T 串iposn-m+1S 串T 串T 串ijj>mijijiji>njT 串一、简单算法int Index(SString S, SString T, int pos) {// 返回子串T在主串S中第pos个字符之后的位置。若不存在,则函数值为0。// 其中,T非空,1≤pos≤StrLength(S)。i = pos; j = 1;while (i <= S[0] && j <= T[0]) {if (S[i] == T[j]) { ++i; ++j; }// 继续比较后继字符else { i = i-j+2; j = 1; } // 指针后退重新开始匹配}if (j > T[0]) return i-T[0];else return 0;} // Index先比较模式串的第一个字符,再比较模式串的最后一个字符,最后比较模式串中从第二个到第 n-1 个字符。二、首尾匹配算法int Index_FL(SString S, SString T, int pos) {sLength = S[0]; tLength = T[0];i = pos;patStartChar = T[1]; patEndChar = T[tLength];while (i <= sLength – tLength + 1) {if (S[i] != patStartChar) ++i; //重新查找匹配起始点else if (S[i+tLength-1] != patEndChar) ++i;// 模式串的“尾字符”不匹配else { }}return 0;}检查中间字符的匹配情况k = 1; j = 2;while ( j < tLength && S[i+k] == T[j]){ ++k; ++j; }if ( j == tLength ) return i;else ++i;// 重新开始下一次的匹配检测KMP算法的时间复杂度可以达到O(m+n)当 S[i] <> T[j] 时,已经得到的结果:S[i-j+1..i-1] == T[1..j-1]若已知 T[1..k-1] == T[j-k+1..j-1]则有 S[i-k+1..i-1] == T[1..k-1]三、KMP(D.E.Knuth, V.R.Pratt,J.H.Morris) 算法定义:模式串的next函数int Index_KMP(SString S, SString T, int pos) {// 1≤pos≤StrLength(S)i = pos; j = 1;while (i <= S[0] && j <= T[0]) {if (j = 0 || S[i] == T[j]) { ++i; ++j; }// 继续比较后继字符else j = next[j]; // 模式串向右移动}if (j > T[0]) return i-T[0]; // 匹配成功else return 0;} // Index_KMP这实际上也是一个匹配的过程,不同在于:主串和模式串是同一个串求 next 函数值的过程是一个递推过程,分析如下:已知:next[1] = 0;假设:next[j] = k;又 T[j] = T[k]则: next[j+1] = k+1若: T[j] T[k]则 需往前回朔,检查 T[j] = T[ ?]void get_next(SString &T, int &next[] ) {// 求模式串T的next函数值并存入数组next。i = 1; next[1] = 0; j = 0;while (i < T[0]) {if (j == 0 || T[i] == T[j]){++i; ++j; next[i] = j; }else j = next[j];}} // get_next还有一种特殊情况需要考虑:例如:S = aaabaaabaaabaaabaaab T = aaaab next[j]= 01234nextval[j]=00004void get_nextval(SString &T, int &nextval[]) {i = 1; nextval[1] = 0; j = 0;while (i < T[0]) {if (j == 0 || T[i] == T[j]) {++i; ++j;if (T[i] != T[j]) next[i] = j;else nextval[i] = nextval[j];}else j = nextval[j];}} // get_nextval1. 熟悉串的七种基本操作的定义,并能利用这些基本操作来实现串的其它各种操作的方法。2. 熟练掌握在串的定长顺序存储结构上实现串的各种操作的方法。3. 了解串的堆存储结构以及在其上实现串操作的基本方法。4. 理解串匹配的KMP算法,熟悉NEXT函数的定义,学会手工计算给定模式串的NEXT函数值和改进的NEXT函数值。5. 了解串操作的应用方法和特点。QUICK QUIZ(4)一、基本知识题1. 空串与空格串有何区别?2. 已知两个串为A=’ac cab cabcbbca’B=’abc’判断B串是否是A串的子串,如果是其子串,说明起始点是A串的第几个字符。3. 串是一种特殊的线性表,其特殊性体现在什么地方?4. 串的两种最基本的存储方式是什么?5. 两个串相等的充分必要条件是什么?6.已知串为T = abcaabbabcab ,求next[j].二、算法设计题1. 对于采用顺序结构存储的串r,编写一个函数删除其值等于ch的所有字符。2. 对于采用顺序结构存储的串r,编写一个函数删除r中第i个字符开始的j个字符。3. 对于采用顺序结构存储的串r,设计一算法将串逆置。4. 采用单链表结构存储的串r,编写一个函数将其中所有的’c’字符替换成’s’字符。5. 已知两个采用单链表结构存储的串A和B。试编写一个函数将串B插入到串A中第k个字符之后。返回习题解答(4)一、基本知识题答案1. 答:空串是指长度为零的串;空格串是指包含一个或多个空白字符“ ”(空格键)的字符串。2. 答:B串是A串的子串,其起始点是A串的第9个字符。3. 答:串是一种特殊的线性表,其特殊性体现在串的数据元素是一个字符。4. 答:串的两种最基本的存储方式为顺序存储方式和链式存储方式。5. 答:两个串相等的充分必要条件是两个串的长度相等且对应位置的字符相同。6. 答: next[j]= 011122312345二、算法设计题答案1. 解:本题的算法思想是:从头到尾扫描串,对于值为ch的元素采用移动的方式进行删除。其函数如下:void delelech(orderstring *r,char ch){int i,j;for(i=0;ilen;i++)if(r->vec[i]==ch){for(j=i;jlen;j++)r->vec[j]=r->vec[j+1];r->len=r->len-1;}}2. 解:本题的算法思想是:先判定串中要删除的内容是否存在,若存在则将i+j-1之后的字符前移j个位置。其函数如下:void deletesub(orderstring *r,int i,int j){int k;if(i+j-1>r->len)printf("出界\n");else{for(k=i+j;k<=r->len;k++)r->vec[k-j]=r->vec[k];r->len=r->len-j;}}3. 解:本题的算法思想是:设两个变量分别指向串首及串尾,将它们所指的数据互换,然后将它们逐渐向中间移动直至相遇即可。实现本题功能的函数如下:void inverse(orderstring *r){int i,j;char temp,i=0;j=r->len-1;{temp=r->vec[i];r->vec[i]=r->vec[j];r->vec[j]=temp;i++;j--;}}4. 解:本题采用的算法是:逐一扫描r的每个结点,对于每个数据域为c的结点修改其元素值为s。其对应的函数如下:void replace(linkstring *r,char c,char s);{linkstring *p;p=r;while(p!=NULL){if(p->data=='c')p->data='s';p=p->link;}}5. 解:实现本题功能的函数如下:void insert(linkstring *A,linkstring *B,int k){int i=1;linkstring *p,*q;p=A;while(i{p=p->link;i++;}if(p==NULL)printf("k值错!\n");else{q=B;while(q!=NULL)q=q->link;q->link=p->link;p->link=B;}}返回(共196张PPT)何谓查找表 ?查找表是由同一类型的数据元素(或记录)构成的集合。由于“集合”中的数据元素之间存在着松散的关系,因此查找表是一种应用灵便的结构。对查找表经常进行的操作:1)查询某个“特定的”数据元素是否在查找表中;2)检索某个“特定的”数据元素的各种属性;3)在查找表中插入一个数据元素;4)从查找表中删去某个数据元素。仅作查询和检索操作的查找表。静态查找表有时在查询之后,还需要将“查询”结果为“不在查找表中”的数据元素插入到查找表中;或者,从查找表中删除其“查询”结果为“在查找表中”的数据元素。动态查找表查找表可分为两类:是数据元素(或记录)中某个数据项的值,用以标识(识别)一个数据元素(或记录)。关键字若此关键字可以识别唯一的一个记录,则称之谓“主关键字”。若此关键字能识别若干记录,则称之谓“次关键字”。根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素或(记录)查找若查找表中存在这样一个记录,则称“查找成功”,查找结果:给出整个记录的信息,或指示该记录在查找表中的位置;否则称“查找不成功”,查找结果:给出“空记录”或“空指针”。由于查找表中的数据元素之间不存在明显的组织规律,因此不便于查找。为了提高查找的效率, 需要在查找表中的元素之间人为地 附加某种确定的关系,换句话说, 用另外一种结构来表示查找表。如何进行查找?查找的方法取决于查找表的结构。9.1 静态查找表9.2 动态查找树表9.3 哈希表9.1静 态 查 找 表数据对象D:数据关系R:D是具有相同特性的数据元素的集合。每个数据元素含有类型相同的关键字,可唯一标识数据元素。数据元素同属一个集合。ADT StaticSearchTable {Create(&ST, n);Destroy(&ST);Search(ST, key);Traverse(ST, Visit());基本操作 P:next} ADT StaticSearchTable构造一个含 n 个数据元素的静态查找表ST。Create(&ST, n);操作结果:销毁表ST。Destroy(&ST);初始条件:操作结果:静态查找表ST存在;若 ST 中存在其关键字等于kval 的数据元素,则函数值为该元素的值或在表中的位置,否则为“空”。Search(ST, kval);初始条件:操作结果:静态查找表ST存在,kval 为和查找表中元素的关键字类型相同的给定值;按某种次序对ST的每个元素调用函数Visit()一次且仅一次,一旦Visit()失败,则操作失败。Traverse(ST, Visit());初始条件:操作结果:静态查找表ST存在,Visit是对元素操作的应用函数;typedef struct {ElemType *elem;// 数据元素存储空间基址,建表时// 按实际长度分配,0号单元留空int length; // 表的长度} SSTable;假设静态查找表的顺序存储结构为数据元素类型的定义为:typedef struct {keyType key; // 关键字域… … // 其它属性域} ElemType ;, TElemType ;一、顺序查找表二、有序查找表三、静态查找树表四、索引顺序表以顺序表或线性链表表示静态查找表一、顺序查找表ST.elem回顾顺序表的查找过程:假设给定值 e = 64,要求 ST.elem[i] = e, 问: i = iiint location( SqList L, ElemType& e,Status (*compare)(ElemType, ElemType)) {i = 1;p = L.elem;while ( i<=L.length &&!(*compare)(*p++,e))) i++;if ( i<= L.length) return i;else return 0;} //locationi<=L.lengthST.elemiST.elemi60ikval = 64kval = 60i64int Search_Seq(SSTable ST,KeyType kval) {// 在顺序表ST中顺序查找其关键字等于// key的数据元素。若找到,则函数值为// 该元素在表中的位置,否则为0。ST.elem[0].key = kval; // 设置“哨兵”for (i=ST.length; --i);// 从后往前找return i; // 找不到时,i为0} // Search_SeqST.elem[i].key!=kval;定义: 查找算法的平均查找长度(Average Search Length)为确定记录在查找表中的位置,需和给定值进行比较的关键字个数的期望值其中: n 为表长,Pi 为查找表中第i个记录的概率,且 Ci为找到该记录时,曾和给定值比较过的关键字的个数分析顺序查找的时间性能。在等概率查找的情况下,顺序表查找的平均查找长度为:对顺序表而言,Ci = n-i+1ASL = nP1 +(n-1)P2 + +2Pn-1+Pn若查找概率无法事先测定,则查找过程采取的改进办法是,在每次查找之后,将刚刚查找到的记录直接移至表尾的位置上。在不等概率查找的情况下,ASLss 在Pn≥Pn-1≥···≥P2≥P1时取极小值上述顺序查找表的查找算法简单,但平均查找长度较大,特别不适用于表长较大的查找表。二、有序查找表若以有序表表示静态查找表,则查找过程可以基于“折半”进行。ST.elemST.length例如: key = 64 的查找过程如下lowhighmidlowmidhighmidlow 指示查找区间的下界;high 指示查找区间的上界;mid = (low+high)/2。int Search_Bin ( SSTable ST, KeyType kval ) {low = 1; high = ST.length; // 置区间初值while (low <= high) {mid = (low + high) / 2;if (kval == ST.elem[mid].key )return mid; // 找到待查元素else if ( kval < ST.elem[mid].key) )high = mid - 1; // 继续在前半区间进行查找else low = mid + 1; // 继续在后半区间进行查找}return 0; // 顺序表中不存在待查元素} // Search_Bin先看一个具体的情况,假设:n=11分析折半查找的平均查找长度6391425781011判定树12233334444假设 n=2h-1 并且查找概率相等则在n>50时,可得近似结果一般情况下,表长为 n 的折半查找的判定树的深度和含有 n 个结点的完全二叉树的深度相同。关键字: A B C D EPi: 0.2 0.3 0.05 0.3 0.15Ci: 2 3 1 2 3三、静态查找树表在不等概率查找的情况下,折半查找不是有序表最好的查找方法例如:此时 ASL=2 0.2+3 0.3+1 0.05+2 0.3+3 0.15=2.4若改变Ci的值 2 1 3 2 3则 ASL=2 0.2+1 0.3+3 0.05+2 0.3+3 0.15=1.9使达最小的判定树称为最优二叉树,其中:定义:为计算方便,令 wi = pi选择二叉树的根结点,使 达最小介绍一种次优二叉树的构造方法:为便于计算,引入累计权值和并设 wl-1 = 0 和 swl-1 = 0,则推导可得023811151823例如:lh211812431018h9608EC21Ah53lhG3013ECGABDF所得次优二叉树如下所示:查找比较“总次数”= 3 2+4 1+2 5+3 3+1 4+3 3+2 5 = 52查找比较“总次数”= 3 2+2 1+3 5+1 3+3 4+2 3+3 5 = 59和折半查找相比较DBACFEGStatus SecondOptimal(BiTree &T, ElemType R[],float sw[], int low, int high) {// 由有序表R[low..high]及其累计权值表sw// 递归构造次优查找树T。选择最小的ΔPi值if (!(T = new BiTNode))return ERROR;T->data = R[i]; // 生成结点构造次优二叉树的算法if (i==low) T->lchild = NULL; // 左子树空else SecondOptimal(T->lchild, R, sw, low, i-1);// 构造左子树if (i==high) T->rchild = NULL; // 右子树空else SecondOptimal(T->rchild, R, sw, i+1, high);// 构造右子树return OK;} // SecondOptimal次优查找树采用二叉链表的存储结构Status CreateSOSTre(SOSTree &T, SSTable ST) {// 由有序表 ST 构造一棵次优查找树 T。// ST 的数据元素含有权域 weightif (ST.length = 0) T = NULL;else {FindSW(sw, ST);// 按照有序表 ST 中各数据元素// 的 weight 值求累计权值表SecondOpiamal(T, ST.elem, sw, 1, ST.length);}return OK;} // CreatSOSTree四、索引顺序表在建立顺序表的同时,建立一个索引。0 1 2 3 4 5 6 7 8 9 10 11 12 13 ……17 08 21 19 14 31 33 22 25 40 52 61 78 46 ……21 0 40 5 78 10 ……例如:索引顺序表 = 索引 + 顺序表顺序表索引typedef struct {KeyType maxkey;int stadr;}indexItem; // 索引项typedef struct {indexItem *elem;int stadr;}indexTable; // 索引表索引顺序表的查找过程:1)由索引确定记录所在区间;2)在顺序表的某个区间内进行查找。可见,索引顺序查找的过程也是一个“缩小区间”的查找过程。顺序表 有序表表的特性 无序 有序存储结构 顺序 或 链式 顺序插删操作 易于进行 需移动元素ASL的值 大 小对比顺序表和有序表的查找性能:索引顺序查找的平均查找长度 =查找“索引”的平均查找长度+ 查找“顺序表”的平均查找长度注意:索引可以根据查找表的特点来构造。ADT DynamicSearchTable {抽象数据类型动态查找表的定义如下:数据对象D:数据关系R:数据元素同属一个集合。D是具有相同特性的数据元素的集合。每个数据元素含有类型相同的关键字, 可唯一标识数据元素。InitDSTable(&DT)基本操作P:DestroyDSTable(&DT)SearchDSTable(DT, key);InsertDSTable(&DT, e);DeleteDSTable(&T, key);TraverseDSTable(DT, Visit());next}ADT DynamicSearchTable操作结果:构造一个空的动态查找表DT。InitDSTable(&DT);销毁动态查找表DT。DestroyDSTable(&DT);初始条件:操作结果:动态查找表DT存在;若DT中存在其关键字等于 key的数据元素,则函数值为该元素的值或在表中的位置,否则为“空”。SearchDSTable(DT, key);初始条件:操作结果:动态查找表DT存在,key为和关键字类型相同的给定值;动态查找表DT存在,e 为待插入的数据元素;InsertDSTable(&DT, e);初始条件:操作结果:若DT中不存在其关键字等于 e.key 的 数据元素,则插入 e 到DT。动态查找表DT存在,key为和关键字类型相同的给定值;DeleteDSTable(&T, key);初始条件:操作结果:若DT中存在其关键字等于key的数据元素,则删除之。动态查找表DT存在,Visit是对结点操作的应用函数;TraverseDSTable(DT, Visit());初始条件:操作结果:按某种次序对DT的每个结点调用函数 Visit() 一次且至多一次。一旦 Visit() 失败,则操作失败。9.2动 态 查 找 树 表 (n) (1) (n) (1) (nlogn)综合上一节讨论的几种查找表的特性:查找 插入 删除无序顺序表无序线性链表有序顺序表有序线性链表静态查找树表 (n) (n) (logn) (n) (logn) (1) (1) (n) (1) (nlogn)1)从查找性能看,最好情况能达 (logn),此时要求表有序;2)从插入和删除的性能看,最好情况能达 (1),此时要求存储结构是链表。可得如下结论:一、二叉排序树(二叉查找树)二、二叉平衡树三、B - 树四、B+树五、键 树一、二叉排序树(二叉查找树)1.定义2.查找算法3.插入算法4.删除算法5.查找性能的分析(1)若它的左子树不空,则左子树上所有结点的值均小于根结点的值;1.定义:二叉排序树或者是一棵空树;或者是具有如下特性的二叉树:(3)它的左、右子树也都分别是二叉排序树。(2)若它的右子树不空,则右子树上所有结点的值均大于根结点的值;503080209010854035252388例如:是二叉排序树。66不通常,取二叉链表作为二叉排序树的存储结构typedef struct BiTNode { // 结点结构struct BiTNode *lchild, *rchild;// 左右孩子指针} BiTNode, *BiTree;TElemType data;2.二叉排序树的查找算法:1)若给定值等于根结点的关键字,则查找成功;2)若给定值小于根结点的关键字,则继续在左子树上进行查找;3)若给定值大于根结点的关键字,则继续在右子树上进行查找。否则若二叉排序树为空,则查找不成功;50308020908540358832例如:二叉排序树查找关键字== 50 ,505035 ,503040355090 ,50809095 ,从上述查找过程可见,在查找过程中,生成了一条查找路径:从根结点出发,沿着左分支或右分支逐层向下直至关键字等于给定值的结点;或者从根结点出发,沿着左分支或右分支逐层向下直至指针指向空树为止。——查找成功——查找不成功算法描述如下:Status SearchBST (BiTree T, KeyType key,BiTree f, BiTree &p ) {// 在根指针 T 所指二叉排序树中递归地查找其// 关键字等于 key 的数据元素,若查找成功,// 则返回指针 p 指向该数据元素的结点,并返回// 函数值为 TRUE;} // SearchBST… … … …否则表明查找不成功,返回// 指针 p 指向查找路径上访问的最后一个结点,// 并返回函数值为FALSE, 指针 f 指向当前访问// 的结点的双亲,其初始调用值为NULLif (!T)else if ( EQ(key, T->data.key) )else if ( LT(key, T->data.key) )else{ p = f; return FALSE; } // 查找不成功{ p = T; return TRUE; } // 查找成功SearchBST (T->lchild, key, T, p );// 在左子树中继续查找SearchBST (T->rchild, key, T, p );// 在右子树中继续查找fT设 key = 48fTfT22pfTfTTTTfffp30201040352523根据动态查找表的定义,“插入”操作在查找不成功时才进行;3.二叉排序树的插入算法若二叉排序树为空树,则新插入的结点为新的根结点;否则,新插入的结点必为一个新的叶子结点,其插入位置由查找过程得到。Status Insert BST(BiTree &T, ElemType e ){// 当二叉排序树中不存在关键字等于 e.key 的// 数据元素时,插入元素值为 e 的结点,并返// 回 TRUE; 否则,不进行插入并返回FALSEif (!SearchBST ( T, e.key, NULL, p )){ }else return FALSE;} // Insert BST… …s = new BiTNode;// 为新结点分配空间s->data = e;s->lchild = s->rchild = NULL;if ( !p ) T = s; // 插入 s 为新的根结点else if ( LT(e.key, p->data.key) )p->lchild = s; // 插入 *s 为 *p 的左孩子else p->rchild = s; // 插入 *s 为 *p 的右孩子return TRUE; // 插入成功(1)被删除的结点是叶子;(2)被删除的结点只有左子树或者只有右子树;(3)被删除的结点既有左子树,也有右子树。4.二叉排序树的删除算法可分三种情况讨论:和插入相反,删除在查找成功之后进行,并且要求在删除二叉排序树上某个结点之后,仍然保持二叉排序树的特性。50308020908540358832(1)被删除的结点是叶子结点例如:被删关键字 = 2088其双亲结点中相应指针域的值改为“空”50308020908540358832(2)被删除的结点只有左子树或者只有右子树其双亲结点的相应指针域的值改为 “指向被删除结点的左子树或右子树”。被删关键字 = 408050308020908540358832(3)被删除的结点既有左子树,也有右子树4040以其前驱替代之,然后再删除该前驱结点被删结点前驱结点被删关键字 = 50Status DeleteBST (BiTree &T, KeyType key ) {// 若二叉排序树 T 中存在其关键字等于 key 的// 数据元素,则删除该数据元素结点,并返回// 函数值 TRUE,否则返回函数值 FALSEif (!T) return FALSE;// 不存在关键字等于key的数据元素else { }} // DeleteBST算法描述如下:… …if ( EQ (key, T->data.key) )// 找到关键字等于key的数据元素else if ( LT (key, T->data.key) )else{ Delete (T); return TRUE; }DeleteBST ( T->lchild, key );// 继续在左子树中进行查找DeleteBST ( T->rchild, key );// 继续在右子树中进行查找void Delete ( BiTree &p ){// 从二叉排序树中删除结点 p,// 并重接它的左子树或右子树if (!p->rchild) { }else if (!p->lchild) { }else { }} // Delete其中删除操作过程如下所描述:… …… …… …p// 右子树为空树则只需重接它的左子树q = p; p = p->lchild; delete(q);pqq// 左子树为空树只需重接它的右子树q = p; p = p->rchild; delete(q);ppqqq = p; s = p->lchild;while (!s->rchild) { q = s; s = s->rchild; }// s 指向被删结点的前驱// 左右子树均不空p->data = s->data;if (q != p ) q->rchild = s->lchild;else q->lchild = s->lchild;// 重接*q的左子树delete(s);pqs5.查找性能的分析对于每一棵特定的二叉排序树,均可按照平均查找长度的定义来求它的 ASL 值,显然,由值相同的 n 个关键字,构造所得的不同形态的各棵二叉排序树的平均查找长 度的值不同,甚至可能差别很大。由关键字序列 3,1,2,5,4构造而得的二叉排序树由关键字序列 1,2,3,4,5构造而得的二叉排序树,例如:2134535412ASL =(1+2+3+4+5)/ 5= 3ASL =(1+2+3+2+3)/ 5= 2.2下面讨论平均情况:不失一般性,假设长度为 n 的序列中有 k 个关键字小于第一个关键字,则必有 n-k-1 个关键字大于第一个关键字,由它构造的二叉排序树n-k-1k的平均查找长度是 n 和 k 的函数P(n, k) ( 0 k n-1 )假设 n 个关键字可能出现的 n! 种排列的可能性相同,则含 n 个关键字的二叉排序树的平均查找长度在等概率查找的情况下,由此可类似于解差分方程,此递归方程有解:二、二叉平衡树何谓“二叉平衡树”?二叉平衡树的查找性能分析如何构造“二叉平衡树”二叉平衡树是二叉查找树的另一种形式,其特点为:树中每个结点的左、右子树深度之差的绝对值不大于1 。例如:548254821是平衡树不是平衡树构造二叉平衡(查找)树的方法是:在插入过程中,采用平衡旋转技术。例如:依次插入的关键字为5, 4, 2, 8, 6, 95424258665842向右旋转一次先向右旋转再向左旋转426589426895向左旋转一次继续插入关键字 9在平衡树上进行查找的过程和二叉排序树相同,因此,查找过程中和给定值进行比较的关键字的个数不超过平衡 树的深度。平衡树的查找性能分析:问:含 n 个关键字的二叉平衡树可能达到的最大深度是多少?n = 0空树最大深度为 0n = 1最大深度为 1n = 2最大深度为 2n = 4最大深度为 3n = 7最大深度为 4先看几个具体情况:反过来问,深度为 h 的二叉平衡树中所含结点的最小值 Nh 是多少?h = 0N0 = 0h = 1h = 2h = 3一般情况下,N1 = 1N2 = 2N3 = 4Nh = Nh-1 + Nh-2 + 1利用归纳法可证得,Nh = Fh+2 - 1因此,在二叉平衡树上进行查找时,查找过程中和给定值进行比较的关键字的个数和 log(n) 相当。由此推得,深度为 h 的二叉平衡树中所含结点的最小值 Nh = h+2/5 - 1反之,含有 n 个结点的二叉平衡树能达到的最大深度 hn = log ( 5 (n+1)) - 2三、 B - 树1.定义2.查找过程3.插入操作4.删除操作5.查找性能的分析1.B-树的定义B-树是一种 平衡 的 多路 查找 树:在 m 阶的B-树上,每个非终端结点可能含有:n 个关键字 Ki(1≤ i≤n) nn 个指向记录的指针 Di(1≤i≤n)n+1 个指向子树的指针 Ai(0≤i≤n);多叉树的特性typedef struct BTNode {int keynum; // 结点中关键字个数,结点大小struct BTNode *parent;// 指向双亲结点的指针KeyType key[m+1]; // 关键字(0号单元不用)struct BTNode *ptr[m+1]; // 子树指针向量Record *recptr[m+1]; // 记录指针向量} BTNode, *BTree; // B树结点和B树的类型B-树结构的C语言描述如下:非叶结点中的多个关键字均自小至大有序排列,即:K1< K2 < … < Kn;且 Ai-1 所指子树上所有关键字均小于Ki;Ai 所指子树上所有关键字均大于Ki;查找树的特性平衡树的特性树中所有叶子结点均不带信息,且在树中的同一层次上;根结点或为叶子结点,或至少含有两棵子树;其余所有非叶结点均至少含有 m/2 棵子树,至多含有 m 棵子树;从根结点出发,沿指针搜索结点和在结点内进行顺序(或折半)查找 两个过程交叉进行。2.查找过程:若查找成功,则返回指向被查关键字所在结点的指针和关键字在结点中的位置;若查找不成功,则返回插入位置。typedef struct {BTNode *pt; // 指向找到的结点的指针int i; // 1..m,在结点中的关键字序号int tag; // 标志查找成功(=1)或失败(=0)} Result; // 在B树的查找结果类型假设返回的是如下所述结构的记录:Result SearchBTree(BTree T, KeyType K) {// 在m 阶的B-树 T 中查找关键字 K, 返回// 查找结果 (pt, i, tag)。若查找成功,则// 特征值 tag=1, 指针 pt 所指结点中第 i 个// 关键字等于 K; 否则特征值 tag=0, 等于// K 的关键字应插入在指针 pt 所指结点// 中第 i 个关键字和第 i+1个关键字之间} // SearchBTree… …p=T; q=NULL; found=FALSE; i=0;while (p && !found) {n=p->keynum; i=Search(p, K);// 在p->key[1..keynum]中查找 i , p->key[i]<=Kkey[i+1]if (i>0 && p->key[i]==K) found=TRUE;else { q=p; p=p->ptr[i]; } // q 指示 p 的双亲}if (found) return (p,i,1); // 查找成功else return (q,i,0); // 查找不成功在查找不成功之后,需进行插入。显然,关键字插入的位置必定在最下层的非叶结点,有下列几种情况:3.插入1)插入后,该结点的关键字个数n不修改指针; 例如2)插入后,该结点的关键字个数 n=m,则需进行“结点分裂”,令 s = m/2 ,在原结点中保留(A0,K1,。。。, Ks-1,As-1);建新结点(As,Ks+1,。。。 ,Kn,An);将(Ks,p)插入双亲结点;例如3)若双亲为空,则建新的根结点。例如例如:下列为 3 阶B-树50 20 40 80 插入关键字 = 60, 60 80 90, 60 80 90 90 50 80 60 30, 40 20 30 50 80803050和插入的考虑相反,首先必须找到待删关键字所在结点,并且要求删除之后,结点中关键字的个数不能小于 m/2 -1,否则,要从其左(或右)兄弟结点“借调”关键字,若其左和右兄弟结点均无关键字可借(结点中只有最少量的关键字),则必须进行结点的“合并”。4.删除在B-树中进行查找时,其查找时间主要花费在搜索结点(访问外存)上,即主要取决于B-树的深度。5.查找性能的分析问:含 N 个关键字的 m 阶 B-树可能达到的最大深度 H 为多少?第 2 层 2 个先推导每一层所含最少结点数:第 1 层 1 个第 H+1 层 2 ( m/2 ) H-1 个第 4 层 2 ( m/2 )2 个第 3 层 2 m/2 个反过来问: 深度为H的B-树中,至少含有多少个结点?… …假设 m 阶 B-树的深度为 H+1,由于第 H+1 层为叶子结点,而当前树中含有 N 个关键字,则叶子结点必为 N+1 个,N+1≥2( m/2 )H-1H-1≤log m/2 ((N+1)/2)H≤log m/2 ((N+1)/2)+1由此可推得下列结果:在含 N 个关键字的 B-树上进行一次查找,需访问的结点个数不超过log m/2 ((N+1)/2)+1结论:是B-树的一种变型四、B+树1.B+树的结构特点:※ 每个叶子结点中含有 n 个关键字和 n 个指向记录的指针;并且,所有叶子结点彼此相链接构成一个有序链表,其头指针指向含最小关键字的结点;※ 每个非叶结点中的关键字 Ki 即为其相应指针 Ai 所指子树中关键字的最大值;※ 所有叶子结点都处在同一层次上,每个叶子结点中关键字的个数均介于 m/2 和 m 之间。2.查找过程※在 B+ 树上,既可以进行缩小范围的查找,也可以进行顺序查找;※ 在进行缩小范围的查找时,不管成功与否,都必须查到叶子结点才能结束;※若在结点内查找时,给定值≤Ki, 则应继续在 Ai 所指子树中进行查找;3.插入和删除的操作类似于B-树进行,即必要时,也需要进行结点的“分裂”或“归并”。50 9615 5062 78 9671 7884 89 9656 6220 26 43 503 8 15sqroot五、键 树1. 键树的结构特点2. .双链树3. Trie树1. 键树的结构特点:※ 关键字中的各个符号分布在从根结点到叶的路径上,叶结点内的符号为“结束”的标志符。因此,键树的深度和关键字集合的大小无关;※ 键树被约定为是一棵有序树,即同一层中兄弟结点之间依所含符号自左至右有序,并约定结束符‘$’小于任何其它符号。HAD$S$VE$E$R$E$IGH$S$例如:表示关键字集合{HAD, HAS, HAVE, HE, HER, HERE, HIGH, HIS }2. 双链树— 以二叉链表作存储结构实现的键树typedef enum { LEAF, BRANCH }NodeKind;// 两种结点:{叶子 和 分支}结点结构:first symbol next分支结点infoptr symbol next叶子结点指向孩子结点的指针指向兄弟结点的指针指向记录的指针 H AD$HADE$R$$ES$GH$I HEHERHEREHIGHHIS…T 叶子结点分支结点含关键字的记录typedef struct DLTNode {char symbol;struct DLTNode *next; // 指向兄弟结点的指针NodeKind kind;union {Record *infoptr; // 叶子结点内的记录指针struct DLTNode *first;// 分支结点内的孩子链指针}} DLTNode, *DLTree; // 双链树的类型#define MAXKEYLEN 16//关键字的最大长度typedef struct {char ch[MAXKEYLEN]; // 关键字int num; // 关键字长度} KeysType; // 关键字类型在双链树中查找记录的过程:假设: T 为指向双链树根结点的指针,K.ch[0..K.num-1] 为待查关键字(给定值)。则查找过程中的基本操作为进行下列比较:K.ch[i] = p->symbol其中: p 指向双链树中某个结点,0 ≤ i ≤ K.num-1初始状态: p=T->first; i = 0;若 ( p && p->symbol == K.ch[i] && i则继续和给定值的下一位进行比较p=p->first; i++;若 ( p && p->symbol != K.ch[i] )则继续在键树的同一层上进行查找 p=p->next;若 ( p && p->symbol==K.ch[i] && i==K.num-1)则 查找成功,返回指向相应记录的指针 p->infoptr若 ( p == NULL)则表明查找不成功,返回“空指针”;3. Trie树— 以多重链表作存储结构实现的键树结点结构:分支结点叶子结点指向记录的指针0 1 2 3 4 5 … … 24 25 26关键字指向下层结点的指针每个域对应一个“字母”0 1(A) 3 4 5(E) 9(I) … … 268(H)4(D) 19(S) 22(V) 0 18(R) 7(G) 190 5(E)THADHASHAVEHEHERHEREHIGHHIS 叶子结点分支结点指向记录的指针typedef struct TrieNode {NodeKind kind; // 结点类型union {struct { KeyType K; Record *infoptr } lf;// 叶子结点(关键字和指向记录的指针)struct { TrieNode *ptr[27]; int num } bh;// 分支结点(27个指向下一层结点的指针)}} TrieNode, *TrieTree; // 键树类型结点结构的 C 语言描述:在 Trie 树中查找记录的过程:假设: T 为指向 Trie 树根结点的指针,K.ch[0..K.num-1] 为待查关键字(给定值)。则查找过程中的基本操作为:搜索和对应字母相应的指针:若 p 不空,且 p 所指为分支结点,则 p = p->bh.Ptr[ord(K.Ch[i])] ;( 其中: 0 ≤ i ≤ K.num-1 )初始状态: p=T; i = 0;若 ( p && p->kind == BRANCH && i则继续搜索下一层的结点p=p->bh.ptr[ord(K.ch[i])]; i++;其中,ord 为求字符在字母表中序号的函数若 ( p && p->kind==LEAF && p->lf.K==K)则 查找成功,返回指向相应记录的指针 p->lf.infoptr反之,即 ( !p || p->kind==LEAF && p->lf.K!=K )则表明查找不成功,返回“空指针”;一、哈希表是什么?二、哈希函数的构造方法三、处理冲突的方法四、哈希表的查找五、哈希表的删除操作六、对静态查找表,。。。9.3 哈 希 表以上两节讨论的表示查找表的各种结构的共同特点:记录在表中的位置和它的关键字之间不存在一个确定的关系,一、哈希表是什么?查找的过程为给定值依次和关键字集合中各个关键字进行比较,查找的效率取决于和给定值进行比较的关键字个数。用这类方法表示的查找表,其平均查找长度都不为零不同的表示方法,其差别仅在于:关键字和给定值进行比较的顺序不同。只有一个办法:预先知道所查关键字在表中的位置,对于频繁使用的查找表,希望 ASL = 0。即,要求:记录在表中位置和其关键字之间存在一种确定的关系。若以下标为000 ~ 999 的顺序表表示之。例如:为每年招收的 1000 名新生建立一张查找表,其关键字为学号,其值的范围为 xx000 ~ xx999 (前两位为年份)。则查找过程可以简单进行:取给定值(学号)的后三位,不需要经过比较便可直接从顺序表中找到待查关键字。但是,对于动态查找表而言,因此在一般情况下,需在关键字与记录在表中的存储位置之间建立一个函数关系,以 f(key) 作为关键字为 key 的记录在表中的位置,通常称这个函数 f(key) 为哈希函数。1) 表长不确定;2) 在设计查找表时,只知道关键字所属范围,而不知道确切的关键字。{Zhao, Qian, Sun, Li, Wu, Chen, Han, Ye, Dei}例如:对于如下 9 个关键字设 哈希函数 f(key) = (Ord(第一个字母) -Ord('A')+1)/2 ChenZhaoQianSunLiWuHanYeDei问题: 若添加关键字 Zhou , 怎么办?能否找到另一个哈希函数?1) 哈希函数是一个映象,即:将关键字的集合映射到某个地址集合上,它的设置很灵活,只要这个地址集合的大小不超出允许范围即可;从这个例子可见,2) 由于哈希函数是一个压缩映象,因此,在一般情况下,很容易产生“冲突”现象,即: key1 key2,而 f(key1) = f(key2)。3) 很难找到一个不产生冲突的哈希函数。一般情况下,只能选择恰当的哈希函数,使冲突尽可能少地产生。因此,在构造这种特殊的“查找表” 时,除了需要选择一个“好”(尽可能少产生冲突)的哈希函数之外;还需要找到一种“处理冲突” 的方法。哈希表的定义:根据设定的哈希函数 H(key) 和所选中的处理冲突的方法,将一组关键字映象到一个有限的、地址连续的地址集 (区间) 上,并以关键字在地址集中的“象”作为相应记录在表中的存储位置,如此构造所得的查找表称之为“哈希表”。二、构造哈希函数的方法对数字的关键字可有下列构造方法:若是非数字关键字,则需先对其进行数字化处理。1. 直接定址法3. 平方取中法5. 除留余数法4. 折叠法6. 随机数法2. 数字分析法哈希函数为关键字的线性函数H(key) = key 或者H(key) = a key + b1. 直接定址法此法仅适合于:地址集合的大小 = = 关键字集合的大小此方法仅适合于:能预先估计出全体关键字的每一位上各种数字出现的频度。2. 数字分析法假设关键字集合中的每个关键字都是由 s 位数字组成 (u1, u2, …, us),分析关键字集中的全体, 并从中提取分布均匀的若干位或它们的组合作为地址。以关键字的平方值的中间几位作为存储地址。求“关键字的平方值” 的目的是“扩大差别” ,同时平方值的中间各位又能受到整个关键字中各位的影响。3. 平方取中法此方法适合于:关键字中的每一位都有某些数字重复出现频度很高的现象。将关键字分割成若干部分,然后取它们的叠加和为哈希地址。有两种叠加处理的方法:移位叠加和间界叠加。4. 折叠法此方法适合于:关键字的数字位数特别多。5. 除留余数法设定哈希函数为:H(key) = key MOD p其中, p≤m (表长) 并且p 应为不大于 m 的素数或是不含 20 以下的质因子给定一组关键字为: 12, 39, 18, 24, 33, 21,若取 p=9, 则他们对应的哈希函数值将为:3, 3, 0, 6, 6, 3例如:为什么要对 p 加限制?可见,若 p 中含质因子 3, 则所有含质因子 3 的关键字均映射到“3 的倍数”的地址上,从而增加了“冲突”的可能。6.随机数法设定哈希函数为:H(key) = Random(key)其中,Random 为伪随机函数通常,此方法用于对长度不等的关键字构造哈希函数。实际造表时,采用何种构造哈希函数的方法取决于建表的关键字集合的情况(包括关键字的范围和形态),总的原则是使产生冲突的可能性降到尽可能地小。三、处理冲突的方法“处理冲突” 的实际含义是:为产生冲突的地址寻找下一个哈希地址1. 开放定址法2. 链地址法为产生冲突的地址 H(key) 求得一个地址序列:H0, H1, H2, …, Hs 1≤ s≤m-1其中:H0 = H(key)Hi = ( H(key) + di ) MOD mi=1, 2, …, s1. 开放定址法对增量 di 有三种取法:1) 线性探测再散列 di = c i 最简单的情况 c=12) 平方探测再散列 di = 12, -12, 22, -22, …,3) 随机探测再散列 di 是一组伪随机数列 或者di=i×H2(key) (又称双散列函数探测)即:产生的 Hi 均不相同,且所产生的s(m-1)个 Hi 值能覆盖哈希表中所有地址。则要求:注意:增量 di 应具有“完备性”※ 随机探测时的 m 和 di 没有公因子。※ 平方探测时的表长 m 必为形如 4j+3的素数(如: 7, 11, 19, 23, … 等);例如: 关键字集合{ 19, 01, 23, 14, 55, 68, 11, 82, 36 }设定哈希函数 H(key) = key MOD 11 ( 表长=11 )1901231455681901231468若采用线性探测再散列处理冲突若采用二次探测再散列处理冲突118236551182361 1 2 1 3 6 2 5 1H2(key) 是另设定的一个哈希函数,它的函数值应和 m 互为素数。若 m 为素数,则 H2(key) 可以是 1 至 m-1 之间的任意数;若 m 为 2 的幂次,则H2(key) 应是 1 至 m-1 之间的任意奇数。例如,当 m=11时,可设 H2(key)=(3 key) MOD 10+11901231455681182362 1 1 1 2 1 2 1 3将所有哈希地址相同的记录都链接在同一链表中。2. 链地址法0123456 11 198268 55 1436 01 23 ASL=(6×1+2×2+3)/9=13/9例如:同前例的关键字,哈希函数为 H(key)=key MOD 7查找过程和造表过程一致。假设采用开放定址处理冲突,则查找过程为:四、哈希表的查找对于给定值 K, 计算哈希地址 i = H(K)若 r[i] = NULL 则查找不成功若 r[i].key = K 则查找成功否则 “求下一地址 Hi” ,直至r[Hi] = NULL (查找不成功)或 r[Hi].key = K (查找成功) 为止。int hashsize[] = { 997, ... };typedef struct {ElemType *elem;int count; // 当前数据元素个数int sizeindex;// hashsize[sizeindex]为当前容量} HashTable;#define SUCCESS 1#define UNSUCCESS 0#define DUPLICATE -1//--- 开放定址哈希表的存储结构 ---Status SearchHash (HashTable H, KeyType K,int &p, int &c) {// 在开放定址哈希表H中查找关键码为K的记录} // SearchHashp = Hash(K); // 求得哈希地址while ( H.elem[p].key != NULLKEY &&!EQ(K, H.elem[p].key))collision(p, ++c); // 求得下一探查地址 pif (EQ(K, H.elem[p].key)) return SUCCESS;// 查找成功,返回待查数据元素位置 pelse return UNSUCCESS; // 查找不成功Status InsertHash (HashTable &H, Elemtype e){} // InsertHashc = 0;if ( HashSearch ( H, e.key, p, c ) == SUCCESS )return DUPLICATE;// 表中已有与 e 有相同关键字的元素elseH.elem[p] = e; ++H.count; return OK;// 查找不成功时,返回 p为插入位置else RecreateHashTable(H); // 重建哈希表if ( c < hashsize[H.sizeindex]/2 ) {// 冲突次数 c 未达到上限,(阀值 c 可调)}1) 选用的哈希函数;2) 选用的处理冲突的方法;3) 哈希表饱和的程度,装载因子 α=n/m 值的大小(n—记录数,m—表的长度)决定哈希表查找的ASL的因素:哈希表查找的分析:从查找过程得知,哈希表查找的平均查找长度实际上并不等于零。一般情况下,可以认为选用的哈希函数是“均匀”的,则在讨论ASL时,可以不考虑它的因素。因此,哈希表的ASL是处理冲突方法和装载因子的函数。例如:前述例子线性探测处理冲突时, ASL =双散列探测处理冲突时,ASL =链地址法处理冲突时, ASL =22/914/913/9线性探测再散列链地址法随机探测再散列可以证明:查找成功时有下列结果:从以上结果可见,哈希表的平均查找长度是 的函数,而不是 n 的函数。这说明,用哈希表构造查找表时,可以选择一个适当的装填因子 ,使得平均查找长度限定在某个范围内。—— 这是哈希表所特有的特点。从哈希表中删除记录时,要作特殊处理,相应地,需要修改查找的算法。五、哈希表的删除操作六、哈希表也可以用来构造静态查找表。并且,对静态查找表,有时可以找到不发生冲突的哈希函数。即,此时的哈希表的 ASL=0, 称此类哈希函数为理想(perfect)的哈希函数。1. 顺序表和有序表的查找方法及其平均查找长度的计算方法。2. 静态查找树的构造方法和查找算法,理解静态查找树和折半查找的关系。3. 熟练掌握二叉排序树的构造和查找方法。4. 理解B-树,B+树和键树的特点以及它们的建树和查找的过程。5. 熟练掌握哈希表的构造方法,深刻理解哈希表与其它结构的表的实质性的差别。6. 掌握按定义计算各种查找方法在等概率情况下查找成功时的平均查找长度。习题与练习一、基础知识题1. 解释下列名词(1) 查找 (2) 树型查找 (3) 平衡因子(4) 散列函数 (5) 冲突2. 设有序表为{a,b,c,d,e,f,g},请分别画出对给定值f,g和h进行拆半查找的过程。3. 试述顺序查找法、二分查找法和分块查找法对被查找表中元素的要求,每种查找法对长度为n的表的等概率查找长度是多少?4. 设散列表长m=14,哈希函数为H(k)=k mod 11,表中一共有8个元素{15,27,50,73,49,61,37,60} ,试画出采用二次探测法处理冲突的散列表。5. 线性表的关键字集合为{113,12,180,138,92,67,94,134,252,6,70,323,60},共有13个元素,已知散列函数为:H(k)=k mod 13,采用链接表处理冲突,试设计这种链表结构。6. 设关键字集合为{27,49,79,5,37,1,56,65,83},散列函数为:H(k)=k mod 7,散列表长度m=10,起始地址为0,分别用线性探测和链接表法来解决冲突。试画出对应的散列表。二、算法设计题1. 从小到大排列的,试写出对此链表的查找算法,并说明是否可以采用折半查找。2. 如果线性表中各结点查找概率不等,则可以使用下面的策略提高顺序表的查找效率:如果找到指定的结点,则将该结点和其前趋(若存在)结点交换,使得经常被查找的结点尽量位于表的前端。试对线性表的顺序存储结构和链式存储结构写出实现上述策略的顺序查找算法(注意查找时必须从表头开始向后扫描)。3. 试设计一个在用开放地址法解决冲突的散列表上删除一个指定结点的算法。4. 设给定的散列表存储空间为H[1~m],每个单元可存放一个记录,H[i](1≤i≤m)的初始值为零,选取散列函数为H(R.key),其中key为记录R的关键字,解决冲突方法为线性探测法,编写一个函数将某记录R填入到散列表H中。返回习题解答一、基本知识题答案1. 答:(1)查找:查找又称为查询或检索,是在一批记录中依照某个域的指定域值,找出相应的记录的操作。(2)树型查找:将原始数据表示成二叉排序树,树的每个结点对应一个记录,利用此二叉排序树进行类似于二分查找思想的数据查找,这种查找方法称为树型查找。(3)平衡因子:二叉树中每一结点的左子树高度减右子树高度为该结点的平衡因子。(4)散列函数:根据关键字求存储地址的函数称为散列函数。(5)两个不同的关键字,其散列函数值相同,因而被映射到同一个表位置上的现象称为冲突。2. 答:查找f的过程如下:查找成功,找到k=f值查找g的过程如下:查找h的过程如下:查找成功,找到k=g值查找不成功3. 答:顺序查找法:表中元素可以任意存放。查找成功的平均查找长度为(n+1)/2。二分查找法:表中元素必须以关键字的值递增或递减地存放且只能以顺序表存放。查找成功的平均查找长度为log2(n+1)-1。分块查找法:表中每块内的元素可以任意存放,但块与块之间必须按关键字的大小递增或递减地存放,即前一块内所有元素的关键字不能大(或小)于后一块内任意一个元素的关键字。若用顺序查找确定所在块,平均查找长度为1/2(n/s+s)+1;若用二分查找确定所在块,平均查找长度为log2(n/s+1)+s/2。4. 答:采用二次探测法处理冲突的散列表如下:5. 答:由题意,可得:H(113) = 113 % 13 =9H(12) = 12 % 13 =12H(180) = 180 % 13 =11H(138) =138 % 13 =8H(92) = 92 % 13 =1H(67) = 67 % 13 =2H(94) = 94% 13 =3H(134) = 134% 13 =4H(252) = 252 % 13 =5H(6) = 6% 13 =6H(70) = 70 % 13 =5H(323) = 323% 13 =11H(60) = 60 % 13 =8链接表法的散列表如下图所示:链接表法的散列表如下图所示:6. 答:线性探测法的散列表如下图所示:二、算法设计题答案1. 解:实现本题功能的算法如下,如果查找成功,则返回指向关键字为x的结点的指针,否则返回NULL。node *sqsearch(node *head,int x){node *p=head;while(p!=NULL)if(x>p->key)p=p->link;else if(x==p->key)return p;else{p=NULL;return p;}}虽然链表中的结点是按递增顺序排列的,但是其存储结构为单链表,查找结点时只能从头指针开始逐步进行搜索,所以不能用折半查找。2. 解:采用顺序存储结构的算法如下,设记录存储在线性表的1~n单元中。如果查找成功,返回关键字为k的记录在线性表中的位置,如果失败则返回0。int seqsearch(sqlist r,int n,int k){int i,j;i=1;while((r[i].key!=k) && (i<=n))i++;if(i<=n){r[0]=r[i];r[i]=r[i-1];r[i-1]=r[i];i--;return(i);}elsereturn(0);}采用链式存储结构的算法如下。如果查找成功,则返回指向关键字为k的结点的指针,否则返回NULL。node *seqsearch(node *head,int k){if(head->key==k)return(head);else{node *p,*q;int x;p=head;q=head->link;while(q!=NULL && q->key!=k){p=q;q=q->link;}if(q!=NULL){x=p->key;p->key=q->key;q->key=x;q=p;}return(q);}}3. 解:本题的算法思想是:首先计算要删除的关键字为k的记录所在的位置,将其置为空(即删除),然后利用线性探测法查找是否有与k发生冲突而存储到下一地址的记录,如果有则将记录移到原来k所在的位置,直至表中没有与k冲突的记录为止。实现本题功能的算法如下:void delete(sqlist r,int n,int k){int h,h0,h1;h=k%n;while(r[h].key!=k)h=(h+1)%n;r[h]=NULL;h0=h;h1=(h+1)%n;while(h1!=h){ while(r[h1].key%n!=h)h1=(h1+1)%n;r[h0]=r[h1];r[h1]=NULL;h0=h1;h1=(h1+1)%n;}}4. 解:本题的算法思想:先计算地址H(R.key),如果没有冲突,则直接填入;否则利用线性探测法求出下一地址,直到找到一个为零的地址,然后填入。实现本题功能的函数如下:void insert(record H,int m,record R){int i;i=H(R.key);if(H[i]==NULL)H[i]=R;else{while(H[i]!=NULL){i=(i+1)%(m+1);}H[i]=R;}}返回欢迎访问水电知识网·中国水利水电出版社http://http://www.获取更多图书源代码和电子教案高职教材《数据结构(C语言描述)》(王路群主编) 源代码绪论P8例: 计算f=1!+2!+3!+…+n!,用C语言描述。void factorsum(n)int n;{int i,j;int f,w;f=0;for (i=1;i〈=n;i++){w=1;for (j=1;j〈=i;j++)w=w*j;f=f+w;}return;}第二章 线性表P16【算法2.1 顺序表的插入】int Insert(Elemtype List[],int *num,int i,Elemtype x){/*在顺序表List[]中,*num为表尾元素下标位置,在第i个元素前插入数据元素x,若成功,返回TRUE,否则返回FALSE。*/int j;if (i<0||i>*num+1){printf(“Error!”); /*插入位置出错*/return FALSE;}if (*num>=MAXNUM-1){printf(“overflow!”);return FALSE;} /*表已满*/for (j=*num;j>=i;j--)List[j+1]=List[j]; /*数据元素后移*/List[i]=x; /*插入x*/(*num)++; /*长度加1*/return TRUE;}P18【算法2.2 顺序表的删除】int Delete(Elemtype List[],int *num,int i){/*在线性表List[]中,*num为表尾元素下标位置,删除第i个长度,线性表的长度减1,若成功,则返回TRUE;否则返回FALSE。*/int j;if(i<0||i>*num){printf(“Error!”); return FALSE; } /*删除位置出错!*/for(j=i+1;j<=*num;j++)List[j-1]=List[j]; /*数据元素前移*/(*num)--; /*长度减1*/return TRUE; }P19 例:将有序线性表La={2,4,6,7,9},Lb={1,5,7,8},合并为Lc={1,2,4,5,6,7,7,8,9}。void merge(Elemtype La[],Elemtype Lb[],Elemtype **Lc){ int i,j,k;int La_length,Lb_length;i=j=0;k=0;La_length=Length(La);Lb_length=Length(Lb); /*取表La,Lb的长度*/Initiate(Lc); /*初始化表Lc*/While (i<=La_length&&j<=Lb_length){ a=get(La,i);b=get(Lb,j);if(aelse {insert(Lc,++k,b);++j;}} /*将La和Lb的元素插入到Lc中*/while (i<=La_length) { a=get(La,i);insert(Lc,++k,a);}while (j<=lb_length) { b=get(La,j);insert(Lc,++k,b); } }P21例如:下面定义的结点类型中,数据域包含三个数据项:学号、姓名、成绩。Struct student{ char num[8]; /*数据域*/har name[8]; /*数据域*/int score; /*数据域*/struct student *next; /*指针域*/}P21单链表结点结构定义为:Typedef struct slnode{ Elemtype data;struct slnode *next;}slnodetype;slnodetype *p,*q,*s;P21 【算法2.3 单链表的初始化】int Initiate(slnodetype * *h){ if((*h=(slnodetype*)malloc(sizeof(slnodetype)))==NULL) return FALSE;(*h)->next=NULL;return TRUE; }P22 【算法2.4 单链表的后插入】{ s=(slnodetype*)malloc(sizeof(slnodetype));s->data=x;s->next=p->next;p->next=s;}P22 【算法2.5 单链表的结点插入】{q=head;while(q->next!=p) q=q->next;s=(slnodetype*)malloc(sizeof(slnodetype));s->data=x;s->next=p;q->next=s;}P23【算法2.6 单链表的前插入】int insert(slnodetype *h,int i,Elemtype x){/*在链表h中,在第i个数据元素前插入一个数据元素x */slnodetype *p,*q,*s;int j=0;p=h;while(p!=NULL&&jnext;j++; /*寻找第i-1个结点*/ }if ( j!=i-1) {printf(“Error!”);return FALSE; /*插入位置错误*/}if ((s=(slnodetype*)malloc(sizeof(slnodetype)))==NULL) return FALSE;s->data=x;s->next=p->next;q->next=s;return TRUE;}P23例:下面C程序中的功能是,首先建立一个线性链表head={3,5,7,9},其元素值依次为从键盘输入正整数(以输入一个非正整数为结束);在线性表中值为x的元素前插入一个值为y的数据元素。若值为x的结点不存在,则将y插在表尾。#include “stdlib.h”#include “stdio.h”struct slnode{int data;struct slnode *next;} /*定义结点类型*/main(){int x,y,d;struct slnode *head,*p,*q,*s;head=NULL; /*置链表空*/q=NULL;scanf(“%d”,&d); /*输入链表数据元素*/while(d>0){p=(struct slnode*)malloc(sizeof(struct slnode)); /*申请一个新结点*/p->data=d;p->next=NULL;if(head==NULL) head=p; /*若链表为空,则将头指针指向当前结点p*/else q->next=p; /*链表不为空时,则将新结点链接在最后*/q=p; /*将指针q指向链表的最后一个结点*/scanf(“%d”,&d);}scanf(“%d,%d”,&x,&y);s=(struct slnode*)malloc(sizeof(struct slnode));s->data=y;q=head;p=q->next;while((p!=NULL)&&(p->data!=x)) {q=p;p=p->next;} /*查找元素为x的指针*/s->next=p;q->next=s; /*插入元素y*/}P24【算法2.7 单链表的删除】int Delet(slnodetype *h,int i){ /*在链表h中删除第i个结点*/slnodetype *p,*s;int j;p=h;j=0;while(p->next!=NULL&&j{ p=p->next;j=j+1; /*寻找第i-1个结点,p指向其前驱*/}if(j!=i-1){printf(“Error!”); /*删除位置错误!*/return FALSE; }s=p->next;p->next=p->next->next; /*删除第i个结点*/free(s); /*释放被删除结点空间*/return TRUE;}P25例:假设已有线性链表La,编制算法将该链表逆置。void converse(slnodetype *head){slnodetype *p,*q;p=head->next;head->next=NULL;while(p!=NULL){ q=p->next;p->next=head->next;head->next=p;p=q; }}P27例:将两个循环链表首尾相接。La为第一个循环链表表尾指针,Lb为第二个循环链表表尾指针。合并后Lb为新链表的尾指针。Void merge(slnodetype *La,slnodetype *Lb){ slnodetype *p;p=La->next;Lb->next= La->next;La->next=p->next;free(p);}P29【算法2.8 双向链表的插入】int insert_dul(dlnodetype *head,int i,Elemtype x){/*在带头结点的双向链表中第i个位置之前插入元素x*/dlnodetype *p,*s;int j;p=head;j=0;while (p!=NULL&&j{ p=p->next;j++; }if(j!=i||i<1){ printf(“Error!”);return FALSE;}if((s=(dlnodetype *)malloc(sizeof(dlnodetype)))==NULL) return FALSE;s->data=x;s->prior=p->prior; /*图中步骤①*/p->prior->next=s; /*图中步骤②*/s->next=p; /*图中步骤③*/p->prior=s; /*图中步骤④*/return TRUE;}P30【算法2.9 双向链表的删除】int Delete_dl(dlnodetype *head,int i){ dlnodetype *p,*s;int j;p=head;j=0;while (p!=NULL&&j{ p=p->next;j++; }if(j!=i||i<1){ printf(“Error!”);return FALSE;}s=p;p->prior->next=p->next; /*图中步骤①*/p->next->prior=p->prior; /*图中步骤②*/free(s);return TRUE;}P32【算法2.10 多项式相加】struct poly *add_poly(struct poly *Ah,struct poly *Bh){struct poly *qa,*qb,*s,*r,*Ch;qa=Ah->next;qb=Bh->next; /*qa和qb分别指向两个链表的第一结点*/r=qa;Ch=Ah; /*将链表Ah作为相加后的和链表*/while(qa!=NULL&&qb!=NULL) /*两链表均非空*/{ if (qa->exp==qb->exp) /*两者指数值相等*/{x=qa->coef+qb->coef;if(x!=0){ qa->coef=x;r->next=qa;r=qa;s=qb++;free(s);qa++;} /*相加后系数不为零时*/else {s=qa++;free(s);s=qb++;free(s);} /*相加后系数为零时*/}else if(qa->expexp){ r->next=qa;r=qa;qa++;} /*多项式Ah的指数值小*/else {r->next=qb;r=qb;qb++;} /*多项式Bh的指数值小*/}if(qa==NULL) r->next=qb;else r->next=qa; /*链接多项式Ah或Bh中的剩余结点*/return (Ch);}第三章 栈和队列P35相应的C语言函数是:float fact(int n){float s;if (n= =0||n= =1) s=1;else s=n*fact(n-1);return (s); }P38用C语言定义的顺序存储结构的栈如下:# define MAXNUM <最大元素数>typedef struct {Elemtype stack[MAXNUM];int top; } sqstack;P39【算法3.1 栈的初始化】int initStack(sqstack *s){/*创建一个空栈由指针S指出*/if ((s=(sqstack*)malloc(sizeof(sqstack)))= =NULL) return FALSE;s->top= -1;return TRUE;}P39【算法3.2 入栈操作】int push(sqstack *s, Elemtype x){/*将元素x插入到栈s中,作为s的新栈顶*/if(s->top>=MAXNUM-1) return FALSE; /*栈满*/s->top++;s->stack[s->top]=x;return TRUE;}P39【算法3.3 出栈操作】Elemtype pop(sqstack *s){/*若栈s不为空,则删除栈顶元素*/Elemtype x;if(s->top<0) return NULL; /*栈空*/x=s->stack[s->top];s->top--;return x;}P39【算法3.4 取栈顶元素】Elemtype getTop(sqstack *s){/*若栈s不为空,则返回栈顶元素*/if(s->top<0) return NULL; /*栈空*/return (s->stack[s->top]);}P40【算法3.5 判栈空操作】int Empty(sqstack *s){/*栈s为空时,返回为TRUE;非空时,返回为FALSE*/if(s->top<0) return TRUE;return FALSE;}P40【算法3.6 栈置空操作】void setEmpty(sqstack *s){/*将栈s的栈顶指针top,置为-1*/s->top= -1;}P40 C语言定义的这种两栈共享邻接空间的结构如下:Typedef struct {Elemtype stack[MAXNUM];int lefttop; /*左栈栈顶位置指示器*/int righttop; /*右栈栈顶位置指示器*/} dupsqstack;P41【算法3.7 共享栈的初始化】int initDupStack(dupsqstack *s){/*创建两个共享邻接空间的空栈由指针S指出*/if (s=(dupsqstack*)malloc(sizeof(dupsqstack)))= =NULL) return FALSE;s->lefttop= -1;s->righttop=MAXNUM;return TRUE;}P41【算法3.8 共享栈的入栈操作】int pushDupStack(dupsqstack *s,char status,Elemtype x){*把数据元素x压入左栈(status=’L’)或右栈(status=’R’)*/if(s->lefttop+1= =s->righttop) return FALSE; /*栈满*/if(status=’L’) s->stack[++s->lefttop]=x; /*左栈进栈*/else if(status=’R’) s->stack[--s->righttop]=x; /*右栈进栈*/else return FALSE; /*参数错误*/return TRUE;}P42【算法3.9 共享栈的出栈操作】Elemtype popDupStack(dupsqstack *s,char status){/*从左栈(status=’L’)或右栈(status=’R’)退出栈顶元素*/if(status= =’L’){ if (s->lefttop<0)return NULL; /*左栈为空*/else return (s->stack[s->lefttop--]); /*左栈出栈*/}else if(status= =’R’){ if (s->righttop>MAXNUM-1)return NULL; /*右栈为空*/else return (s->stack[s->righttop++]); /*右栈出栈*/}else return NULL; /*参数错误*/}P42链栈的C语言定义为:typedef struct Stacknode{Elemtype data;Struct Stacknode *next;}slStacktype;P43【算法3.10 单个链栈的入栈操作】int pushLstack(slStacktype *top,Elemtype x){/*将元素x压入链栈top中*/slStacktype *p;if((p=(slStacktype *)malloc(sizeof(slStacktype)))= =NULL) return FALSE; /*申请一个结点*/p->data=x; p->next=top; top=p; return TRUE;}P43【算法3.11 单个链栈的出栈操作】Elemtype popLstack(slStacktype *top){/*从链栈top中删除栈顶元素*/slStacktype *p;Elemtype x;if (top= =NULL) return NULL; /*空栈*/p=top; top=top->next;x=p->data;free(p);return x;}P44【算法3.12 多个链栈的入栈操作】int pushDupLs(slStacktype *top[M],int i,Elemtype x){/*将元素x压入链栈top[i]中*/slStacktype *p;if((p=(slStacktype *)malloc(sizeof(slStacktype)))= =NULL) return FALSE; /*申请一个结点*/p->data=x; p->next=top[i]; top[i]=p; return TRUE;}P44【算法3.13 多个链栈的出栈操作】Elemtype popDupLs(slStacktype *top[M],int i){/*从链栈top[i]中删除栈顶元素*/slStacktype *p;Elemtype x;if (top[i]= =NULL) return NULL; /*空栈*/p=top[i]; top[i]=top[i]->next;x=p->data;free(p);return x;}P47【算法3.14 中缀表达式变为后缀表达式】# define MAXNUM 40# define FALSE 0# define TRUE 1#include "stdio.h"#include "stdlib.h"#include "string.h"typedef struct {char stack[MAXNUM];int top; } sqstack;int initStack(sqstack *s){/*初始化栈*/s->top=-1;return TRUE;}int push(sqstack *s,char x){/*将元素x插入到栈s中,作为s的新栈顶*/if(s->top>=MAXNUM-1) return FALSE; /*栈满*/s->top++;s->stack[s->top]=x;return TRUE;}char pop(sqstack *s){/*若栈s不为空,则删除栈顶元素*/char x;if(s->top<0) return NULL; /*栈空*/x=s->stack[s->top];s->top--;return x;}char gettop(sqstack *s){/*若栈s不为空,则返回栈顶元素*/if(s->top<0) return NULL; /*栈空*/return (s->stack[s->top]);}char precede(char x1,char x2){/*比较运算符x1与x2的优先*/char result='<';char sting[2];sting[0]=x2;sting[1]='\0';if (((x1=='+'||x1=='-')&&(strstr("+-)#",sting)!=NULL))||((x1=='*'||x1=='/')&&strstr("+-*/)#",sting)!=NULL)||(x1==')'&&strstr("+-*/)#",sting)!=NULL)){result='>';}else if(x1=='('&&x2==')'||x1=='#'&&x2=='#'){result='=';}else if (x1==')'&&x2=='('||x1=='#'&&x2==')'){result=' ';}return result; }main(){sqstack *optr;char s[80],c,y; int i=0;optr=(sqstack *)malloc(sizeof(sqstack));gets(s);initStack(optr); push(optr,'#');c=s[i];while(c!='#'||gettop(optr)!='#'){if(c!='+'&&c!='-'&&c!='*'&&c!='/'&&c!='('&&c!=')'&&c!='#'){printf("%c",c);c=s[++i];if(c=='\0') break;}elseswitch (precede(gettop(optr),c)){case '<':{push(optr,c);c=s[++i];break;}case '=':{pop(optr);c=s[++i];break; }case '>':{y=pop(optr);printf("%c",y);break;}}} printf("%c",'#');}P51 用C语言定义的顺序存储结构的队列如下:# define MAXNUM <最大元素数>typedef struct {Elemtype queue[MAXNUM];int front; /*队头指示器*/int rear; /*队尾指示器*/} sqqueue;P51【算法3.15 顺序队列的初始化】int initQueue(sqqueue *q){/*创建一个空队列由指针q指出*/if ((q=(sqqueue*)malloc(sizeof(sqqueue)))= =NULL) return FALSE;q->front= -1;q->rear=-1;return TRUE;}P52【算法3.16 顺序队列的入队列操作】int append(sqqueue *q,Elemtype x){/*将元素x插入到队列q中,作为q的新队尾*/if(q->rear>=MAXNUM-1) return FALSE; /*队列满*/q->rear++;q->queue[q->rear]=x;return TRUE;}P52【算法3.17 顺序队列的出队列操作】Elemtype delete(sqqueue *q){/*若队列q不为空,则返回队头元素*/Elemtype x;if(q->rear= =q->front) return NULL; /*队列空*/x=q->queue[++q->front];return x;}P52【算法3.18 顺序队列的取头元素操作】Elemtype getHead(sqqueue *q){/*若队列q不为空,则返回队头元素*/if(q->rear= =q->front) return NULL; /*队列空*/return (q->queue[s->front+1]);}P52【算法3.19 顺序队列的非空判断操作】int Empty(sqqueue *q){/*队列q为空时,返回TRUE;否则返回FALSE*/if (q->rear= =q->front) return TRUE;return FALSE;}P53【算法3.20 顺序队列的求长度操作】int length(sqqueue *q){/*返回队列q的元素个数*/return(q->rear-q->front);}P54用C语言定义循环队列结构如下:typedef struct{Elemtype queue[MAXNUM];int front; /*队头指示器*/int rear; /*队尾指示器*/int s; /*队列标志位*/}qqueue;P54【算法3.21 循环队列的初始化】int initQueue(qqueue *q){/*创建一个空队列由指针q指出*/if ((q=(qqueue*)malloc(sizeof(qqueue)))= =NULL) return FALSE;q->front= MAXNUM;q->rear=MAXNUM;q->s=0; /*置队列空*/return TRUE;}P55【算法3.22 循环队列的入队列操作】int append(qqueue *q,Elemtype x){/*将元素x插入到队列q中,作为q的新队尾*/if (( q->s= =1)&&(q->front= =q->rear)) return FALSE; /*队列满*/q->rear++;if (q->rear= =MAXNUM) q->rear=0;q->queue[q->rear]=x;q->s=1; /*置队列非空*/return TRUE;}P55【算法3.23 循环队列的出队列操作】Elemtype delete(qqueue *q){/*若队列q不为空,则返回队头元素*/Elemtype x;if (q->s= =0) retrun NULL; /*队列为空*/q->front++;if (q->front= =MAXNUM) q->front=0;x=q->queue[q->front];if (q->front = =q->rear) q->s=0; /*置队列空*/return x; }P56 用C语言定义链队列结构如下:typedef struct Qnode{Elemtype data;struct Qnode *next;}Qnodetype; /*定义队列的结点*/typedef struct{ Qnodetype *front;/*头指针*/Qnodetype *rear; /*尾指针*/}Lqueue;P56【算法3.24 链队列的初始化】int initLqueue(Lqueue *q){/*创建一个空链队列q*/if ((q->front=(Qnodetype*)malloc(sizeof(Qnodetype)))= =NULL) return FALSE;q->rear=q->front;q->front->next=NULL;return TRUE;}P56【算法3.25 链队列的入队列操作】int Lappend(Lqueue *q,Elemtype x){/*将元素x插入到链队列q中,作为q的新队尾*/Qnodetype *p;if ((p=(Qnodetype*)malloc(sizeof(Qnodetype)))= =NULL) return FALSE;p->data=x;p->next=NULL; /*置新结点的指针为空*/q->rear->next=p; /*将链队列中最后一个结点的指针指向新结点*/q->rear=p; /*将队尾指向新结点*/return TRUE;}P57【算法3.26 链队列的出队列操作】Elemtype Ldelete(Lqueue *q){/*若链队列q不为空,则删除队头元素,返回其元素值*/Elemtype x;Qnodetype *p;if(q->front->next= =NULL) return NULL; /*空队列*/P=q->front->next; /*取队头*/q->front->next=p->next; /*删除队头结点*/x=p->data;free(p);return x;}串P62 用字符数组存放字符串时,其结构用C语言定义如下:#define MAXNUM <允许的最大的字符数>typedef struct {char str[MAXNUM];int length; /*串长度*/} stringtype; /* 串类型定义*/P62 用链表存放字符串时,其结构用C语言定义如下:typedef struct node{char str;struct node *next;} slstrtype;P63 用块链存放字符串时,其结构用C语言定义如下:typedef struct node{char str[4];struct node *next;} slstrtype;P63 用堆存放字符串时,其结构用C语言定义如下:typedef struct{char *str;int length;} HSstrtype;P65 C语言中用字符数组存储字符串时,结构定义如下:#define MAXNUM 80typedef struct {char str[MAXNUM];int length; /*串长度*/} stringtype; /* 串类型定义*/P65【算法4.1 在静态存储方式中求子串】int substr(stringtype s1,stringtype * s2,int m,int n){int j,k;j=s1.length;if(m<=0||m>j||n<0) {(*s2).str[0]='\0';(*s2).length=0;return FALSE; } /*参数错误*/k=strlen(&s1.str[m-1]) ; /*求子串的长度*/if (n>k) (*s2).length=k;else (*s2).length=n; /*置子串的串长*/for(j=0;j<=(*s2).length;j++,m++) (*s2).str[j]=s1.str[m-1];(*s2).str[j]=’\0’; /*置结束标记*/return TRUE;}P66 假设链表中每个结点仅存放一个字符,则单链表定义如下typedet struct node{char str;struct node *next;} slstrtype;P66【算法4.2 在链式存储方式中求子串】int substr(slstrtype s1,slstrtype *s2,int m,int n){slstrtype *p,*q,*v;int length1,j;p=&s1;for(lenght1=0;p->next!=NULL;p=p->next) length1++; /*求主串和串长*/if(m<=0||m>length1||n<0) {s2=NULL;return FALSE;} /*参数错误*/p=s1.next;for(j=0;jnext; /*找到子串和起始位置*/s2=(slstrtype *)malloc(sizeof(slstrtype)); /*分配子串和第一个结点*/v=s2;q=v;for(j=0;jnext!=NULL;j++) /*建立子串*/{ q->str=p->str;p=p->next;q=(slstrtype *)malloc(sizeof(slstrtype));v->next=q;v=q;}q->str=’\0’;q->next=NULL; /*置子串和尾结点*/return TRUE;}P67 堆存储结构用C语言定义为:typedet struct{char *str;int length;} HSstrtype;P67【算法4.3 共享法求子串】int substr(HSstrtype s1,HSstrtype *s2,int m,int n){ int j,k;j=s1.length;if(m<=0||m>j||n<0) {s2->length=0;return FALSE;} /*参数错误*/k=strlen(s1.str+m); /*主串第m个位置开始之后的串长*/if (n>k) s2->length=k;else s2->length=n; /*置子串的串长*/s2->str=s1.str+m; /*置子串的串首地址return TRUE;}P67【算法4.4 重新赋值法求子串】int substr(HSstrtype s1,HSstrtype *s2,int m,int n){ int j,k;j=s1.length;if(m<=0||m>j||n<0) {s2->length=0;return FALSE;} /*参数错误*/k=strlen(s1.str+m); /*主串第m个位置开始之后的串长*/if (n>k) s2->length=k;else s2->length=n; /*置子串的串长*/k=s2->length;for(j=0;js2->str[j]=s1.str[m++]; /*复制字符*/s2->str[j]=’\0’; /*置结束符*/return TRUE;}P68 例main(){int a,b,c;scanf(〃%d,%d〃,&a,&b);c=a+b;printf(“%d”,c);}多维数组和广义表P77 三元组表#define maxsize 100 /*定义非零元的最大数目*/struct node /*定义一个三元组*/{int i , j; /*非零元行、列号*/int v; /*非零元值*/};struct sparmatrix /*定义稀疏矩阵*/{int rows,cols ; /*稀疏矩阵行、列数*/int terms; /*稀疏矩阵非零元个数*/node data [maxsize]; /*三元组表*/};P79 十字链表的数据类型描述如下:struct linknode{ int i, j;struct linknode *cptr, *rptr;union vnext /*定义一个共用体*/{ int v; /*表结点使用V域,表示非零元值*/struct linknode next; /*表头结点使用next域*/} k; }P81 (1)按照A的列序进行转置#define maxsize 100struct node{int i,j; /*定义三元组的行、列号*/int v; /*三元组的值*/};struct sparmatrix{int rows,cols; /*稀疏矩阵的行、列数*/int terms; /*稀疏矩阵的非零元个数*/struct node data[maxsize]; /*存放稀疏矩阵的三元组表*/};void transpose(struct sparmatrix a){struct sparmatrix b; /*b为a的转置*/int ano,bno=0,col,i;b.rows=a.cols; b.cols=a.rows;b.terms=a.terms;if (b.terms>0){for ( col=0; colfor( ano=0;anoif (a.data[ano].j==col) /*进行转置*/{ b.data[bno].j=a.data[ano].i;b.data[bno].i=a.data[ano].j;b.data[bno].v=a.data[ano].v;bno++;}}for( i=0;iprintf("%5d%5d%5d\n",b.data[i].i,b.data[i].j,b.data[i].v);}void main(){int i;struct sparmatrix a;scanf("%d%d%d",&a.rows,&a.cols,&a.terms); /*输入稀疏矩阵的行、列数及非零元的个数*/for( i=0;iscanf("%d%d%d",&a.data[i].i,&a.data[i].j,&a.data[i].v); /*输入转置前的稀疏矩阵的三元组*/for(i=0;iprintf("%5d%5d%5d\n",a.data[i].i,a.data[i].j,a.data[i].v); /*输出转置前的三元组结果*/transpose( a); /*调用转置算法*/}P83 M稀疏矩阵的转置矩阵N的三元组表#define maxsize 100struct node{int i,j;int v;};struct sparmatrix{int rows,cols;int terms;struct node data[maxsize];};void fastrans(struct sparmatrix a){struct sparmatrix b;int pot[maxsize],col,ano,bno,t,i;b.rows=a.cols; b.cols=a.rows;b.terms=a.terms;if(b.terms>0){for(col=0;col<=a.cols;col++)pot[col]=0;for( t=0;t{col=a.data[t].j;pot[col+1]=pot[col+1]+1;}pot[0]=0;for(col=1;colpot[col]=pot[col-1]+pot[col];for( ano=0;ano{ col=a.data[ano].j;bno=pot[col];b.data[bno].j=a.data[ano].i;b.data[bno].i=a.data[ano].j;b.data[bno].v=a.data[ano].v;pot[col]=pot[col]+1;}}for( i=0;iprintf("%d\t%d\t%d\n",b.data[i].i,b.data[i].j,b.data[i].v); /*输出转置后的三元组*/}void main(){ struct sparmatrix a;int i;scanf("%d%d%d",&a.rows,&a.cols,&a.terms); /*输入稀疏矩阵的行、列数及非零元的个数*/for( i=0;iscanf("%d%d%d",&a.data[i].i,&a.data[i].j,&a.data[i].v); /*输入转置前的三元组*/for(i=0;iprintf("%d\t%d\t%d\n",a.data[i].i,a.data[i].j,a.data[i].v); /*输出转置前的三元组*/fastrans(a); /*调用快速转置算法*/}P85第二步,生成表中结点。算法描述如下:#include#define maxsize 100struct linknode{int i,j;struct linknode *cptr,*rptr;union vnext{ int v;struct linknode *next;} k;};struct linknode *creatlindmat( ) /*建立十字链表*/{ int x, m, n, t, s, i, j, k;struct linknode *p , *q, *cp[maxsize],*hm;printf("请输入稀疏矩阵的行、列数及非零元个数\n");scanf("%d%d%d",&m,&n,&t);if (m>n) s=m; else s=n;hm=(struct linknode*)malloc(sizeof(struct linknode)) ;hm->i=m; hm->j=n;cp[0]=hm;for (i=1; i<=s;i++){ p=(struct linknode*)malloc(sizeof(struct linknode)) ;p->i=0; p->j=0;p->rptr=p; p->cptr=p;cp[i]=p;cp[i-1]->k.next=p;}cp[s]->k.next=hm;for( x=1;x<=t;x++){ printf("请输入一个三元组(i,j,v)\n");scanf("%d%d%d",&i,&j,&k); /*输入一个非零元的三元组*/p=(struct linknode*)malloc(sizeof(struct linknode)); /*生成一个三元组的结点*/p->i=i; p->j=j; p->k.v=k;/*以下是将p插入到第i行链表中 */q=cp[i];while ((q->rptr!=cp[i]) &&( q->rptr->jq=q->rptr;p->rptr=q->rptr;q->rptr=p;/*以下是将P插入到第j列链表中*/q=cp[j];while((q->cptr!=cp[j]) &&( q->cptr->iq=q->cptr;p->cptr=q->cptr;q->cptr=p;}return hm;}void main(){ struct linknode *p,*q;struct linknode *hm;hm=creatlindmat( ); /*生成十字链表*/p=hm->k.next;while(p->k.next!=hm) /*输出十字链表*/{ q=p->rptr;while(q->rptr!=p){ printf("%3d%3d%3d\t",q->i,q->j,q->k.v);q=q->rptr;}if(p!=q)printf("%3d%3d%3d",q->i,q->j,q->k.v);printf("\n");p=p->k.next;}q=p->rptr;while(q->rptr!=p){ printf("%3d%3d%3d\t",q->i,q->j,q->k.v);q=q->rptr;}if(p!=q)printf("%3d%3d%3d",q->i,q->j,q->k.v);printf("\n");}P88 稀疏矩阵十字链表相加算法如下:/*假设ha为A稀疏矩阵十字链表的头指针,hb为B稀疏矩阵十字链表的头指针*/#include#define maxsize 100struct linknode{ int i,j;struct linknode *cptr,*rptr;union vnext{ int v;struct linknode *next;} k;};struct linknode creatlindmat( ) /*建立十字链表*/{ int x, m, n, t, s, i, j, k;struct linknode *p , *q, *cp[maxsize],*hm;printf("请输入稀疏矩阵的行、列数及非零元个数\n");scanf("%d%d%d",&m,&n,&t);if (m>n) s=m; else s=n;hm=(struct linknode*)malloc(sizeof(struct linknode)) ;hm->i=m; hm->j=n;cp[0]=hm;for (i=1; i<=s;i++){ p=(struct linknode*)malloc(sizeof(struct linknode)) ;p->i=0; p->j=0;p->rptr=p; p->cptr=p;cp[i]=p;cp[i-1]->k.next=p;}cp[s]->k.next=hm;for( x=1;x<=t;x++){ printf("请输入一个三元组(i,j,v)\n");scanf("%d%d%d",&i,&j,&k);p=(struct linknode*)malloc(sizeof(struct linknode));p->i=i; p->j=j; p->k.v=k;/*以下是将p插入到第i行链表中 */q=cp[i];while ((q->rptr!=cp[i]) &&( q->rptr->jq=q->rptr;p->rptr=q->rptr;q->rptr=p;/*以下是将P插入到第j列链表中*/q=cp[j];while((q->cptr!=cp[j]) &&( q->cptr->iq=q->cptr;p->cptr=q->cptr;q->cptr=p;}return hm;}/* ha和hb表示的两个稀疏矩阵相加,相加的结果放入ha中*/struct linknode *matadd(struct linknode *ha, struct linknode *hb){ struct linknode *pa, *pb, *qa, *ca,*cb,*p,*q;struct linknode *hl[maxsize];int i , j, n;if((ha->i!=hb->i)||(ha->j!=hb->j))printf("矩阵不匹配,不能相加\n");else{ p=ha->k.next; n=ha->j;for (i=1;i<=n; i++){ hl[i]=p;p=p->k.next;}ca=ha->k.next; cb=hb->k.next;while(ca->i==0){pa=ca->rptr; pb=cb->rptr;qa=ca;while(pb->j!=0){ if((pa->jj)&&(pa->j!=0)){ qa=pa; pa=pa->rptr;}else if ((pa->j>pb->j)||(pa->j==0)) /*插入一个结点*/{ p=(struct linknode*)malloc(sizeof(struct linknode));p->i=pb->i; p->j=pb->j;p->k.v=pb->k.v;qa->rptr=p; p->rptr=pa;qa=p; pb=pb->rptr;j=p->j; q=hl[j]->cptr;while((q->ii)&&(q->i!=0)){ hl[j]=q; q=hl[j]->cptr;}hl[j]->cptr=p; p->cptr=q;hl[j]=p;}else{pa->k.v=pa->k.v+pb->k.v;if(pa->k.v==0) /*删除一个结点*/{ qa->rptr=pa->rptr;j=pa->j; q=hl[j]->cptr;while (q->ii){hl[j]=q; q=hl[j]->cptr;}hl[j]->cptr=q->cptr;pa=pa->rptr; pb=pb->rptr;free(q);}else{ qa=pa; pa=pa->rptr;pb=pb->rptr;}}}ca=ca->k.next; cb=cb->k.next;}}return ha;}void print(struct linknode *ha) /*输出十字链表*/{ struct linknode *p,*q;p=ha->k.next;while(p->k.next!=ha){ q=p->rptr;while(q->rptr!=p){ printf("%3d%3d%3d\t",q->i,q->j,q->k.v);q=q->rptr;}if(p!=q)printf("%3d%3d%3d",q->i,q->j,q->k.v);printf("\n");p=p->k.next;}q=p->rptr;while(q->rptr!=p){ printf("%3d%3d%3d\t",q->i,q->j,q->k.v);q=q->rptr;}if(p!=q)printf("%3d%3d%3d",q->i,q->j,q->k.v);printf("\n");}void main(){struct linknode *ha=NULL,*hb=NULL,*hc=NULL;ha=creatlindmat( ); /*生成一个十字链表ha*/hb=creatlindmat( ); /*生成另一个十字链表hb*/printf("A:\n"); /*输出十字链表ha*/print(ha);printf("\n");printf("B:\n"); /*输出十字链表hb*/print(hb);printf("\n");hc=matadd(ha,hb); /*十字链表相加*/printf("A+B:\n"); /*输出相加后的结果*/print(hc);printf("\n");}P94 数据类型描述如下:#define elemtype charstruct node1{ int atom;struct node1 *link;union{struct node1 *slink;elemtype data;} ds;}P95 数据类型描述如下:struct node2{ elemtype data;struct node2 *link1,*link2;}P96 求广义表的深度depth(LS)int depth(struct node1 *LS){int max=0,dep;while(LS!=NULL){ if(LS->atom==0) //有子表{ dep=depth(LS->ds.slink);if(dep>max) max=dep;}LS=LS->link;}return max+1;}P96 广义表的建立creat(LS)void creat(struct node1 *LS){char ch;scanf(“%c”,&ch);if(ch=='#')LS=NULL;else if(ch=='('){LS=(struct node*)malloc(sizeof(struct node));LS->atom=0;creat(LS->ds.slink);}else{ LS=(struct node*)malloc(sizeof(struct node));LS->atom=1;LS->ds.data=ch;}scanf(“%c”,&ch);if(LS==NULL);else if(ch==',')creat(LS->link);else if((ch==')')||(ch==';'))LS->link=NULL;}P97 输出广义表print(LS)void print(struct node1 *LS){if(LS->atom==0){printf(“(”);if(LS->ds.slink==NULL)printf(“#”);elseprint(LS->ds.slink);}elseprintf(“%c ”,LS->ds.data);if(LS->atom==0)printf(“)”);if(LS->link!=NULL){printf(“;”);print(LS->link);}}P98 该算法的时间复杂度为O(n)。整个完整程序如下:#include#define elemtype charstruct node1{ int atom;struct node1 *link;union{struct node1 *slink;elemtype data;} ds;};void creat(struct node1 LS) /*建立广义表的单链表*/{char ch;scanf("%c",&ch);if(ch=='#')LS=NULL;else if(ch=='('){LS=(struct node1*)malloc(sizeof(struct node1));LS->atom=0;creat(LS->ds.slink);}else{ LS=(struct node1*)malloc(sizeof(struct node1));LS->atom=1;LS->ds.data=ch;}scanf("%c",&ch);if(LS==NULL);else if(ch==',')creat(LS->link);else if((ch==')')||(ch==';'))LS->link=NULL;}void print(struct node1 LS) /*输出广义单链表*/{if(LS->atom==0){printf("(");if(LS->ds.slink==NULL)printf("#");elseprint(LS->ds.slink);}elseprintf("%c",LS->ds.data);if(LS->atom==0)printf(")");if(LS->link!=NULL){printf(";");print(LS->link);}}int depth(struct node1 LS) /*求广义表的深度*/{int max=0;while(LS!=NULL){ if(LS->atom==0){ int dep=depth(LS->ds.slink);if(dep>max) max=dep;}LS=LS->link;}return max+1;}main(){ int dep;struct node1 *p=NULL;creat(p); /*建立广义表的单链表*/print(p); /*输出广义单链表*/dep=depth(p); /*求广义表的深度*/printf("%d\n",dep);}树P109 二叉链表的结点类型定义如下:typedef struct btnode{ anytype data;struct btnode *Lch,*Rch;}tnodetype;P109 三叉链表的结点类型定义如下:typedef struct btnode3{ anytype data;struct btnode *Lch,*Rch,*Parent ;}tnodetype3;P112 C语言的先序遍历算法:void preorder (tnodetype *t)/*先序遍历二叉树算法,t为指向根结点的指针*/{ if (t!=NULL){printf(“%d ”,t->data);preorder(t->lch);preorder(t->rch);}}P113 C语言的中序遍历算法:void inorder(tnodetype *t)/*中序遍历二叉树算法,t为指向根结点的指针*/{if(t!=NULL){inorder(t->lch);printf(“%d ”,t->data);inorder(t->rch);}}P113 C语言的后序遍历算法:void postorder(tnodetype *t)/*后序遍历二叉树算法,t为指向根结点的指针*/{if(t!=NULL){ postorder(t->lch);postorder(t->rch);printf(“%d ”,t->data);}}P114 如果引入队列作为辅助存储工具,按层次遍历二叉树的算法可描述如下:void levelorder(tnodetype *t)/*按层次遍历二叉树算法,t为指向根结点的指针*/{tnodetype q[20]; /*辅助队列*/front=0;rear=0; /*置空队列*/if (t!=NULL){ rear++;q[rear]=t; /*根结点入队*/}while (front!=rear){ front++;t=q [front];printf (“%c\n”,t->data);if (t->lch!=NULL) /*t的左孩子不空,则入队*/{ rear++;q [rear]=t->lch;}if (t->rch!=NULL) /*t的右孩子不空,则入队*/{ rear++;q [rear]=t->rch;}}}P115 以中序遍历的方法统计二叉树中的结点数和叶子结点数,算法描述为:void inordercount (tnodetype *t)/*中序遍历二叉树,统计树中的结点数和叶子结点数*/{ if (t!=NULL){ inordercount (t->lch); /*中序遍历左子树*/printf (“%c\n”,t->data); /*访问根结点*/countnode++; /*结点计数*/if ((t->lch==NULL)&&(t->rch==NULL))countleaf++; /*叶子结点计数*/inordercount (t->rch); /*中序遍历右子树*/}}P115 可按如下方法计算一棵二叉树的深度:void preorderdeep (tnodetype *t,int j)/*先序遍历二叉树,并计算二叉树的深度*/{ if (t!=NULL){ printf (“%c\n”,t->data); /*访问根结点*/j++;if (kpreorderdeep (t->lch,j); /*先序遍历左子树*/preorderdeep (t->rch,j); /*先序遍历右子树*/}}P117 线索二叉树的结点类型定义如下:struct nodexs{anytype data;struct nodexs *lch, *rch;int ltag,rtag; /*左、右标志域*/}P117 中序次序线索化算法void inorderxs (struct nodexs *t)/*中序遍历t所指向的二叉树,并为结点建立线索*/{ if (t!=NULL){ inorderxs (t->lch);printf (“%c\n”,t->data);if (t->lch!=NULL)t->ltag=0;else { t->ltag=1;t->lch=pr;} /*建立t所指向结点的左线索,令其指向前驱结点pr*/if (pr!=NULL){ if (pr->rch!=NULL)pr->rtag=0;else { pr->rtag=1;pr->rch=p;}} /*建立pr所指向结点的右线索,令其指向后继结点p*/pr=p;inorderxs (t->rch);}}P118 在中根线索树上检索某结点的前驱结点的算法描述如下:struct nodexs * inpre (struct nodexs *q)/*在中根线索树上检索q所指向的结点的前驱结点*/{ if (q->ltag==1)p=q->lch;else { r=q->lch;while (r->rtag!=1)r=r->rch;p=r;}return (p);}P119 在中根线索树上检索某结点的后继结点的算法描述如下:struct nodexs * insucc (struct nodexs *q)/*在中根线索树上检索q所指向的结点的后继结点*/{ if (q->rtag==1)p=q->rch;else { r=q->rch;while (r->ltag!=1)r=r->lch;p=r;}return (p);}P120 算法程序用C语言描述如下:void insertbst(t,s)/*将指针s所指的结点插入到以t为根指针的二叉树中*/{ if (t==NULL)t=s; /*若t所指为空树,s所指结点为根*/else if (s->data < t->data)insertbst(t->lch,s); /*s结点插入到t的左子树上去*/elseinsertbst(t->rch,s); /*s结点插入到t的右子树上去*/}P121 二叉排序树结点删除算法的C语言描述如下:void delnode(bt,f,p)/*bt为一棵二叉排序树的根指针,p指向被删除结点,f指向其双亲*//*当p=bt时f为NULL*/{ fag=0; /*fag=0时需修改f指针信息,fag=1时不需修改*/if (p->lch==NULL)s=p->rch; /*被删除结点为叶子或其左子树为空*/else if (p->rch==NULL)s=p->lch;else { q=p; /*被删除结点的左、右子树均非空*/s=p->lch;while (s->rch!=NULL){ q=s;s=s->rch;} /*寻找s结点*/if (q=p)q->lch=s->lch;else q->rch=s->lch;p->data=s->data; /*s所指向的结点代替被删除结点*/DISPOSE(s);Fag=1;}if (fag=0) /*需要修改双亲指针*/{ if (f=NULL)bt=s; /*被删除结点为根结点*/else if (f->lch=p)f->lch=s;else f->rch=s;DISPOSE(p); /*释放被删除结点*/}}图P134 用邻接矩阵表示法表示图,除了存储用于表示顶点间相邻关系的邻接矩阵外,通常还需要用一个顺序表来存储顶点信息。其形式说明如下:# define n 6 /*图的顶点数*/# define e 8 /*图的边(弧)数*/typedef char vextype; /*顶点的数据类型*/typedef float adjtype; /*权值类型*/typedef struct{vextype vexs[n];adjtype arcs[n][n];}graph;P135 建立一个无向网络的算法。CREATGRAPH(ga) /*建立无向网络*/Graph * ga;{int i,j,k;float w;for(i=0;iga ->vexs[i]=getchar(); /*读入顶点信息,建立顶点表*/for(i=0;ifor(j=0;jga ->arcs[i][j]=0; /*邻接矩阵初始化*/for(k=0;k(scanf(”%d%d%f”,&I,&j,&w); /*读入边(vi,vj)上的权w */ga ->arcs[i][j]=w;ga - >arcs[j][i]=w;}} /*CREATGRAPH*/P136 邻接表的形式说明及其建立算法:typedef struct node{int adjvex; /*邻接点域*/struct node * next; /*链域*/}edgenode; /*边表结点*/typedef struct{vextype vertex; /*顶点信息*/edgenode link; /*边表头指针*/}vexnode; /*顶点表结点*/vexnode ga[n];CREATADJLIST(ga) /*建立无向图的邻接表*/Vexnode ga[ ];{int i,j,k;edgenode * s;for(i=o;i(ga[i].vertex=getchar();ga[i].1ink=NULL; /*边表头指针初始化*/}for(k=0;k{scanf(”%d%d”,&i,&j); /*读入边(vi , vj)的顶点对序号*/s=malloc(sizeof(edgenode)); /*生成邻接点序号为j的表结点*s */s-> adjvex=j;s- - >next:=ga[i].Link;ga[i].1ink=s; /*将*s插入顶点vi的边表头部*/s=malloc(size0f(edgende)); /*生成邻接点序号为i的边表结点*s */s ->adjvex=i;s ->next=ga[j].1ink;ga[j].1ink=s; /*将*s插入顶点vj的边表头部*/}} /* CREATADJLIST */P139 分别以邻接矩阵和邻接表作为图的存储结构给出具体算法,算法中g、g1和visited为全程量,visited的各分量初始值均为FALSE。int visited[n] /*定义布尔向量visitd为全程量*/Graph g; /*图g为全程量*/DFS(i) /*从Vi+1出发深度优先搜索图g,g用邻接矩阵表示*/int i;{ int j;printf(“node:%c\n” , g.vexs[i]); /*访问出发点vi+1 */Visited[i]=TRUE; /*标记vi+l已访问过*/for (j=0;jif((g.arcs[i][j]==1) &&(! visited[j]))DFS(j); /*若Vi+l的邻接点vj+l未曾访问过,则从vj+l出发进行深度优先搜索*/} /*DFS*/vexnode gl[n] /*邻接表全程量*/DFSL(i) /*从vi+l出发深度优先搜索图g1,g1用邻接表表示*/int i;{ int j;edgenode * p;printf(“node:%C\n” ,g1[i].vertex);vistited[i]=TRUE;p=g1[i].1ink; /*取vi+1的边表头指针*/while(p !=NULL) /*依次搜索vi+l的邻接点*/{if(! Vistited[p ->adjvex])DFSL(p - >adjvex); /*从vi+1的未曾访问过的邻接点出发进行深度优先搜索*/p=p - >next; /*找vi+l的下一个邻接点*/}} /* DFSL */P142 以邻接矩阵和邻接表作为图的存储结构,分别给出宽度优先搜索算法。BFS(k) /*从vk+l出发宽度优先搜索图g,g用邻接矩阵表示,visited为访问标志向量*/int k;{ int i,j;SETNULL(Q); /*置空队Q */printf(“%c\n”,g.vexs[k]); /*访问出发点vk+l*x/visited[k]=TRUE; /*标记vk+l已访问过*/ENQUEUE(Q,K); /*已访问过的顶点(序号)入队列*/While(!EMPTY(Q)) /*队非空时执行*/{i=DEQUEUE(Q); /*队头元素序号出队列*/for(j=0;jif((g.arcs[i][j]==1)&&(! visited[j])){printf(“%c\n” , g.vexs[j]); /*访问vi+l的未曾访问的邻接点vj+l */visited[j]=TRUE;ENQUEUE(Q,j); /*访问过的顶点入队*/}}} /* BFS */BFSL(k) /*从vk+l出发宽度优先搜索图g1,g1用邻接表表示*/int k{ int i;edgenode * p;SETNULL(Q);printf(“%c\n” , g1[k].vertex);visited[k]=TRUE;ENQUEUE(Q,k);while(! EMPTY(Q));{ i=DEQUEUE(Q);p=g1[i].1ink /*取vi+l的边表头指针*/while(p !=NULL) /*依次搜索vi+l的邻接点*/{ if( ! visited[p - >adjvex]) /*访问vi+l的未访问的邻接点*/{ printf{“%c\n” , g1[p - >adjvex].vertex};visited[p - >adjvex]=TRUE;ENQUEUE(Q,p - >adjvex); /*访问过的顶点入队*/}p=p - >next; /*找vi+l的下一个邻接点*/}}} /*BFSL*/P148 在对算法Prim求精之前,先确定有关的存储结构如下:typdef struct{Int fromvex,endvex; /*边的起点和终点*/float length; /*边的权值*/} edge;float dist[n][n]; /*连通网络的带权邻接矩阵*/edgeT[n-1]; /*生成树*/P149 抽象语句(1)可求精为:for(j=1;j{T[j-1].fromvex=1}; /*紫边的起点为红点*/T[j-1].endvex=j+1; /*紫边的终点为蓝点*/T[j-1].1ength=dist[0][j]; /*紫边长度*/}P149 抽象语句(3)所求的第k条最短紫边可求精为:min=max; /*znax大于任何边上的权值*/for (j=k;jif(T[j].1ength{min=T[j].1ength;m=j; /*记录当前最短紫边的位置*/}P149 抽象语句(4)的求精:e=T[m];T[m]=T[k];T[k]=e, /* T[k]和T[m]交换*/v=T[kl.Endvex]; /* v是刚被涂红色的顶点*/P149 抽象语句(5)可求精为:for(j=k+1;j{d=dist[v-1][T[j].endvex-1]; /*新紫边的长度*/if(d{T[j].1ength=d;T[j].fromvex=v; /*新紫边取代原最短紫边*/}}P150 完整的算法:PRIM() /*从第一个顶点出发构造连通网络dist的最小生成树,结果放在T中*/{int j , k , m , v , min , max=l0000;float d;edge e;for(j=1;j{T[j-1].formvex=1; /*顶点1是第一个加入树中的红点*/T[j-1].endvex=j+1;T[j-1].length=dist[o][j];}for(k=0;k{min=max;for(j=k;jif(T[j].1ength{min=T[j].1ength;m=j;} /*T[m]是当前最短紫边*/}e=T[m];T[m]=T[k];T[k]=e; /*T[k]和T[m]交换后,T[k]是第k条红色树边*/v=T[k].endvex ; /* v是新红点*/for(j=k+1;j{d=dist[v-1][T[j].endvex-1];if(d{T[j].1ength=d;T[j].fromvex=v;}}} /* PRIM */P151 Kruskl算法的粗略描述:T=(V,φ);While(T中所含边数{从E中选取当前最短边(u,v);从E中删去边(u,v);if((u,v)并入T之后不产生回路,将边(u,v)并入T中;}P153 迪杰斯特拉算法实现。算法描述如下:#define max 32767 /*max代表一个很大的数*/void dijkstra (float cost[][n],int v)/*求源点v到其余顶点的最短路径及其长度*/{ v1=v-1;for (i=0;i{ dist[i]=cost[v1][i]; /*初始化dist*/if (dist[i]pre[i]=v;else pre[i]=0;}pre[v1]=0;for (i=0;is[i]=0; /*s数组初始化为空*/s[v1]=1; /*将源点v归入s集合*/for (i=0;i{ min=max;for (j=0;jif (!s[j] && (dist[j]{ min=dist[j];k=j;} /*选择dist值最小的顶点k+1*/s[k]=1; /*将顶点k+1归入s集合中*/for (j=0;jif (!s[j]&&(dist[j]>dist[k]+cost[k][j])){ dist[j]=dist[k]+cost[k][j]; /*修改 V-S集合中各顶点的dist值*/pre[j]=k+1; /*k+1顶点是j+1顶点的前驱*/}} /*所有顶点均已加入到S集合中*/for (j=0;j{ printf(“%f\n%d”,dist[j],j+1;);p=pre[j];while (p!=0){ printf(“%d”,p);p=pre[p-1];}}}P155 弗洛伊德算法可以描述为:A(0)[i][j]=cost[i][j]; //cost为图的邻接矩阵A(k)[i][j]=min{A(k-1) [i][j],A(k-1) [i][k]+A(k-1) [k][j]}其中 k=1,2,…,nP155 弗洛伊德算法实现。算法描述如下:int path[n][n]; /*路径矩阵*/void floyd (float A[][n],cost[][n]){ for (i=0;ifor (j=0;j{ if (cost[i][j]path[i][j]=j;else { path[i][j]=0;A[i][j]=cost[i][j];}}for (k=0;k/*做n次迭代,每次均试图将顶点k扩充到当前求得的从i到j的最短路径上*/for (i=0;ifor (j=0;jif (A[i][j]>(A[i][k]+A[k][j])) /*修改长度和路径*/{ A[i][j]=A[i][k]+A[k][j];path[i][j]=path[i][k];}for (i=0;ifor (j=0;j{ printf (“%f”,A[i][j]); /*输出最短路径的长度*/next=path[i][j]; /*next为起点i的后继顶点*/if (next==0) /*i无后继表示最短路径不存在*/printf (“%d to %d no path.\n”,i+1,j+1);else { printf (“%d”,i+1); /*最短路径存在*/while (next!=j+1) /*打印后继顶点,然后寻找下一个后继顶点*/{ printf (“%d”,next);next =path[next-1][j];}printf (“%d\n”,j+1);}}}查找P163 具体实现图8-1框图的程序段如下:Void seqsrch(struct node r[],int n,int k){ int i=1;r[n+1].key=k; /*设置边界*/while (r[i].key!=k)i=i+1;if (i<=n)printf(“%3d,it is r[%2d]”,k,i);else printf(“%3d not found”,k );}P164 设表的长度为n,表的被查找部分的头为low,尾为high,初始时,low=1,high=n,k为关键字的值。具体算法如下:Void binsrch(struct node r[ ],int n,int k){ int mid,low,high,find;low=1; high=n;find=0; /*置区间初值*/while ((low<=high) && (!find)){ mid=(low+high)/2; /*求中点*/if (k= = r[mid].key)find=1; /*已查到*/else if(k>r[mid].key )low=mid+1; /*在后半区间查找*/else high=mid-1; /*在前半区间查找*/}if (find)return (mid); /*查找成功,返回找到元素的位置*/else return (0); /*查找不成功,返回0标记*/}P170 下面是线性探测法的算法,请注意,算法中设立了一个查找的边界值i,当顺序探测已超过表长,则要翻转到表首继续查找,直到查到i位置才是真正的查完全表。为编程方便,令i=j-1。Define m 100; /*哈希表的长度*/Struct hash{int key;}HT[m];Void linehash(struct hash HT[ ],int k,int p){j=k%p;i=j-1;while ((HT[j].key!=NULL)&&(HT[j].key!=k)&&(j!=i))j=(j+1)%m; /*解决冲突*/if (HT[j].key==k)printf(“succ!%d,%d\n”,k,j);else if (j==i)printf(“overflow!\n”); /*溢出*/else HT[j].key=k; /*插入k*/}P171 链地址散列表的具体算法描述如下:Define m 100; /*表长*/Struct node{int key;struct node * next;}HT[m];Void linkhash(struct node HT[ ],int k,int p){struct node * p,* r,* s;j=k%p;if(HT[j].key==0){HT[j].key=k;HT[j].next=NULL;}else if(HT[j].key==k)printf(“succ!%d,%d\n”,j,k);else {q=HT[j].next;while(q!=NULL)&&(q->key!=k){r=q;q=q->next;}if(q==NULL){s=(struct node *)malloc(sizeof(struct node));s->key=k;s->next=NULL;r->next=s;}else printf(“succ!%d,%d\n”,j,k);}排序P175 线性插入排序void insertsort(struct node r[ n+1],int n)/* r[n+1]为一维数组,其中r[0]为监视哨,r[1]到r[n]为待排序的n个记录,排序好的记录仍放在r中*/{ for(i=2;i<=n;i++) /*共进行n-1趟*/{ r[0]=r[i]; /*r[0]为监视哨,也可做下边循环结束标志*/j=i-1;while(r[j].key>r[0].key){ r[j+1]=r[j];j--;}r[j+1]=r[0];}}P176 折半插入排序void binarysort(struct node r[ n+1],int n)/*按关键字递增的次序对记录r[1],r[2],……,r[n]进行折半插入排序 */{ for(i=2;i<=n;i++){ r[0]=r[i];l=1;h=i-1;while(l<=h){ mid=(l+h)/2;if(r[0].keyh=mid-1;else l=mid+1;}for(j=i-1;j>=l;j--)r[j+1]=r[j];r[l]=r[0];}}P178 希尔排序rectype R[n+d1]; /* R[d1-1]为d1个监视哨*/int d[t]; /* d[0]到d[t-1]为增量序列*/SHELLSORT(R,d)Rectype R[ ];int d[ ];{int i,j,k,h;rectype temp;int maxint=32767; /*机器中最大整数*/for (i=0;iR[i].key=-maxint; /*设置哨兵*/K=0;Do{H=d[k]; /*取本趟增量*/For(i=h+di;i{temp=R[i]}; /*保存待插入记录R[i]*/j=i-h;while(temp.key{R[j+h]=R[j]}; /*后移记录*/j=j-h; /*得到前一记录位置*/}R[j+h]=temp; /*插入R[i]*/} /*本趟排序完成*/k++;} while (h!=1); /*增量为1排序后终止算法*/} /*SHELLSORT*/P180 选择排序算法如下:void selectsort(struct node r[n+1],int n)/*对记录数组r[1]至r[n]进行选择排序*/{ for(i=1;i{ min=i;for(j=i+1;j<=n;j++) /*选择最小元*/if (r[j].keymin=j;if(min!=i){ temp=r[min];r[min]=r[i];r[i]=temp;}}}P183 把左、右子树都是堆的顺序二叉树调整为一个堆,其算法描述如下:void adjust(struct node r[m+1],int m)/* 将文件(r[1],r[2],…,r[m])解释为一棵顺序二叉树,将其中以r[i]为根结点的二叉树调整为一个堆,设以r[i]为根的二叉树的左,右子树已是堆,1≤i≤1[m/2] */{ x=r[i];j=2*i; /*求出r[i]的左孩子r[2*i],即r[j] */while (j<=m) /*有左孩子*/{ if ((jj=j+1; /*左孩子<右孩子*/if (x.key{ r[i]=r[j]; /*大孩子上移到它的双亲位置*/i=j; /*今 r[j]为根结点*/j=2*i; /*求出左孩子*/}else j=m+1 /*根不小于它的任一孩子时,强迫退出while循环*/}r[i]=x; /*将存放在x中的根结点放到r[i]中*/}P183 堆排序算法如下:void heapsort (struct node r[n+1],int m)/*对记录数组r[1]至r[n]进行堆排序*/{ for(i=n/2;i>=1;i--)adjust(r,i,n); /*建立初始堆*/for(j=n;j>=2;j--){ x=r[1]; r[1]=r[j]; /*交换根结点和堆的最后一个结点*/r[j]=x;Adjust (r,1,j-1) /*将r [1]至r[j-1]调整为堆*/}}P185 一次划分及其排序的算法。int partition(r,1,h) /*返回划分后被定的基准记录的位置*/rectype R[ ]; /*对无序区R[1]到R[h]做划分*/int 1,h;{int i,j;rectype temp;i=1;j=h temp=R[i]; /*初始化,temp为基准*/Do{While((R[j].key>=temp.key) && (ij--; /*从右向左扫描,查找第1个关键字小于temp.key的记录*/if(iwhile((R[i].key<=temp.key) && (ii++; /*从左向右扫描,查找第1个关键字大于temp.key的记录*/if(i}quicksort(R,s1,t1) /*对R[s1]到R[t1]*/rectype R[ ];int s1,t1;{int i;if (s1{i= partition (R,s1,t1); /*对R[s1]到R[t1]做划分*/quicksort (R,s1,i-1); /*递归处理左区间*/quicksort (R,i+1,t1); /*递归处理右区间*/}}P188 归并排序MERGE(R,R1,low,mid,high)rectype R[ ],R1[ ];int low,mid,high;{int i,j,k;i=low;j=mid+1;k=low;while((i<=mid)&&(i<=high))if(R[i].key<=R[j].key)R1[k++]=R1[i++];else R1[k++]=R1[j++];while (i<=mid) R1[k++]=R1[i++];while (i<=high) R1[k++]=R1[j++];}P189 这两种特殊情况进行特殊处理。具体算法如下:MERGEPASS(R,R1,length)rectype R[],R1[];int length;{int i,j;i=0;while(i+2*length-1{MERGEPASS(R,R1,i+length-1,i+2*length-1);i=i+2*length}if(i+2*length-1MERGE(R,R1,i,i+length-1,n-1);elsefor(j=i;j}P190 二路归并算法如下:MERGESORT(R)rectype R[ ];int length;{int length;length=1;while(length{MERGEPASS(R,R1,length);length=2*length;MERGEPASS(R1,R,length);length=2*length;}}数据结构实验与习题内 容 简 介数据结构是计算机专业的核心课,是重要的专业基础课。实践是学习本课程的一个重要的环节。目前各种“数据结构”教材较为注重理论的叙述与介绍,算法描述不拘泥某种语言的语法细节,默认读者已具备扎实的程序设计基础,可以在课下独立完成数据结构实验。实际上在读者群中程序设计的基础并不一致,相当一部分人基础较为薄弱。多数学生反映数据结构的上机实验存在一定的困难,希望有合适的实验参考书指导学习。数据结构的理论学习也有一定的深度,存在一定的难度。学生必须完成一定数量的思考题、练习题、书面作业题,一方面巩固基本知识、一方面提高联系实际分析解决问题的能力。正是基于以上的原因编写了这本“数据结构实验与习题”。本参考书包括C语言基础知识、上机实验习题和书面作业练习题三部分。在C语言基础知识部分,主要介绍了输入/输出、函数及参数传递和结构体的概念应用。这部分内容非常重要,掌握的是否熟练会直接影响“数据结构“的学习。在实验部分,包括有完整的C语言源程序例题,介绍了一些设计数据结构题目所需的C语言常用的知识和技巧。在实验题中,既有简单容易的验证题,即验证已经给出的源程序,或者扩充已经给出的源程序,也有需独立思考设计的综合实验题。在习题部分,既有选择题、判断题,也有用图表解答的练习题、算法设计题或综合解答分析题。并且配有部分练习题的答案供学生自学、练习、参考。由于时间仓足、水平有限,书中难免存在错误和不妥之处,敬请读者指正。目 录第一部分 C语言基本知识一 基本输入和输出---------------------------------------------------------------------------1二 函数与参数传递---------------------------------------------------------------------------3三 结构体及运用 ----------------------------------------------------------------------------5第二部分 上机实验习题上机实验要求及规范------------------------------------------------------------------- 8实习一 复数ADT及其实现-------------------------------------------10实习二 线性表----------------------------------------------------12实习三 栈和队列--------------------------------------------------20实习四 串--------------------------------------------------------28实习五 数组------------------------------------------------------30实习六 树与二叉树------------------------------------------------32实习七 图--------------------------------------------------------34实习八 查找------------------------------------------------------40实习九 排序------------------------------------------------------42第三部分 书面作业练习题习题一 绪论------------------------------------------------------------48习题二 顺序表示(线性表、栈和队列)------------------------------------51习题三 链表(线性表、栈和队列)----------------------------------------54习题四 串--------------------------------------------------------------57习题五 数组------------------------------------------------------------58习题六 树与二叉树------------------------------------------------------60习题七 图--------------------------------------------------------------69习题八 查找------------------------------------------------------------75习题九 排序------------------------------------------------------------78第一部分 C语言基本知识如何选择描述数据结构和算法的语言是十分重要的问题。传统的方法是用PASCAL语言,由于该语言语法规范、严谨,非常适用于数据结构课程教学。在Windows 环境下涌现出一系列的功能强大、面向对象的程序开发工具,如:Visual C++, Borland C++, Visual Basic, Visual Foxpro等。由于Visual Delphi的出现,使PASCAL仍不失为一种优秀的算法描述工具。 近年来在计算机科学研究、系统开发、教学以及应用开发中,C语言的使用越来越广泛。因此,本教材采用类C语言进行算法描述。按照传统的数据结构教材写法,只是注重算法思想和方法。并不关心具体使用何种语言工具来实现,默认学生已经能够具备扎实的程序设计基础和能力。随着计算机科学的发展、教学改革的深化,数据结构的开课时间各个高校有所不同,普遍有所提前。大学生入学起点就存在一定的差异,即使在大学一年级学习了某种程序设计语言,学生中能力和水平的差异依然存在。实践表明在数据结构教学过程中,如果学生的程序设计语言基础薄弱,就会影响正常教学进度。数据结构不仅具有较强的理论性,更具有较强的实践性。当前国内、国外一些优秀的数据结构教材已经是兼顾理论和实践两个方面。因此,有必要将数据结构所必须使用的C语言语法在此做简单介绍。根据多年教学实践,学生完成上机实验练习时遇到的主要问题是,不能正确的输入数据,结构体概念陌生,函数的传址调用概念不清,指针与链表有的没有学过。由于篇幅所限,这里仅对前三个问题加以介绍。如果学生基础好,可以越过这一部分内容不看。一、基本输入和输出对于重要的数据结构算法,均要求进行上机实验。而上机实践中离不开数据的输入/输出。看起来简单的输入/输出,往往是上机实验最容易出错的地方,尤其是输入。对于一个算法程序,如果数据不能正确输入,算法设计得再好也无法正常运行。输入C语言的输入是由系统提供的scanf()等函数实现, 在程序的首部一般要求写入:# include 因为标准输入/输出函数都存在于头文件 stdio.h 之中,现将其包含进来方可使用这些常用的输入/输出函数。有的系统允许不使用上述包含语句,可以直接使用标准输入/输出函数。函数scanf()的功能很丰富,输入格式也是多种多样,这是大家较为熟悉的知识,这里不做详细介绍。在使用中需要注意以下几个问题。一条scanf()语句有多个变量、并且都是数值型(int, float, double)时,在输入数据时应该在一行之内键入多个数据,数据之间空格分隔。例如:int n; float x;scanf (“%d %f ” , &n, &x);正确的输入应是:整数 空格 实数 回车。例如:就是在两个数据之间使用空格键为分隔符,最后打回车键。如果语句中在%d 和%f 之间有一个逗号:scanf (“%d ,%f ” , &n, &x);正确的输入应是:整数 逗号 实数 回车。例如:在需要字符型变量或字符串输入时,要单独写一条输入语句,这样不易出错。如果在同一条scanf()语句中将字符型和数值型混合输入常常会出错。因为键盘输入时在数值型数据之间‘空格键’起‘分隔符’作用,但是在字符或字符串之间,‘空格’会被当做一个字符,而不能起到‘分隔符’的作用。所以将它们混在一起容易出错。(3)在scanf()语句中变量写法应该是该变量的地址,这一点常被忽视。请看下列程序:1: viod main()2: { char name[10], ch ;3: int num; float x;4: printf(“\n 请输入姓名:”); scanf(“%s”, name);5: printf(“\n 请输入性别:”); scanf(“%c”, &ch);6: printf(“\n 请输入学号和成绩:”); scanf(“ %d%f”, &n, &x);……;}为了方便说明问题程序中加了行号,运行时当然不允许行号。一般情况下在scanf()语句中的变量名之前要加上求地址符&,上述程序第5,6行之中就是这样。为什么第4行的name前面不加&呢?因为name代表字符串,即是一维字符数组,一维数组名本身就是一个地址,是该数组的首地址,所以name前面不加&。在本程序中把字符串、字符、数值型变量分别写入不同的scanf()语句,输入数据的具体形式如下:请输入姓名:ZhangHua请输入性别:v请输入学号和成绩:101 90.5请考虑如果姓名输入成:Zhang Hua,会出现什么现象?那样只会读入Zhang做姓名,而Hua被忽略,还会影响后面的输入语句无法正确读入数据。因此,应该充分重视数据的输入技术。输出C语言的输出是由系统提供的printf()等函数来实现, 在程序的首部一般要求写入:# include 因为标准输入/输出函数都存在于头文件 stdio.h 之中,现将其包含进来方可使用这些常用的输入/输出函数。有的系统允许不使用上述包含语句,可以直接使用标准输入/输出函数。输出函数printf()的语法一般容易掌握,这里强调的是怎样合理巧妙的使用它。在连续输出多个数据时,数据之间一定要有间隔,不能连在一起。int n=10, m=20, p=30;printf(“\n %d%d%d”,n,m,p);printf(“\n %6d%6d%6d”,n,m,p); //提倡使用的语句第一行输出是: 102030第二行输出是: 10 20 30在输入语句scanf()之前先使用printf()输出提示信息,但是在printf()最后不能使用换行符。int x;printf(“\n x= ”); //句尾不应使用换行符scanf( “%d”,&x);这样使光标与提示信息出现在同一行上,光标停在问号后边:X= □ 。在该换行的地方,要及时换行。int i;printf(“数据输出如下:\n”); // 需要换行for (i=0; i<8; i++) printf(“%6d”, i ); // 几个数据在同一行输出,不能换行4. 在调试程序时多加几个输出语句,以便监视中间运行状况。程序调式成功后,再去掉这些辅助输出语句。二、函数与参数传递函数的设计和调用是程序设计必不可少的技能,是程序设计最重要的基础。一些初学者之之所以感到编程难,就是忽视了这个基础。在传统的面向过程的程序设计中,往往提倡模块化结构化程序设计,不论BASIC、 FONFTRAN、PASCAL还是其他高级语言,最终要涉及到子函数的设计和使用。C语言的源程序是由一个主函数和若干(或零个)子函数构成,函数是组成C语言程序的基本单位。函数具有相对独立的功能,可以被其他函数调用,也可调用其他函数。当函数直接或间接的调用自身时,这样的函数称为递归函数。是否能够熟练的设计和使用函数,是体现一个人程序设计能力高低的基本条件。因此有必要回顾和复习C语言函数的基本概念。1函数的设计函数设计的一般格式是:类型名 函数名(形参表){ 函数体;}函数设计一般是处理一些数据获得某个结果,因此函数可以具有返回值,上面的类型名就是函数返回值的类型,可以是int, float…..等。例如:float funx(形参表){ 函数体;.}函数也可无返回值,此时类型是void。例如:void funy(形参表){ 函数体;}而函数体内所需处理的数据往往通过形参表传送,函数也可以不设形参表,此时写为:类型名 函数名(void){ 函数体;}例1.2 设计一个函数计算三个整数之和,再设计一个函数仅输出一条线。设计主函数调用两个函数。#include int sumx (int a, int b, int c) /* 计算三个整数之和的函数 */{ int s;s=a+b+c;return s;}void display(void) /* 输出一条线的函数 */{ printf(”----------------------\n“);}void main( ){ int x,y, z ,sa;x=y=z=2;display(); /* 画一条线 */printf(“\n sum=%d”,sumx(x,y,z)); /* 在输出语句中直接调用函数sumx( ) */printf(“\n %6d%6d%6d”,x,y,z);display();x=5; y=6; z=7;sa=sumx(x, y, z); /* 在赋值语句中调用函数sumx( ) */printf(“\n “ sum=%d”,sa);printf(“\n %6d%6d%6d”,x,y,z);display();} /* 程序结束 */运行结果:----------------------sum= 62 2 2----------------------sum=4815 16 17----------------------2. 关于函数的参数传递函数在被调用时,由主调程序提供实参,将信息传递给形参。在调用结束后,有时形参可以返回新的数据给主调程序。这就是所谓参数传递。各种算法语言实现参数传递的方法通常分为传值和传址两大类。在上例中函数sumx()的设计和主函数对它的调用,就是传值调用。第一、第二次调用,带入的实参均是三个整型变量。调用函数返回后,在主程序中输出实参的值仍与调用之前相同。传值调用的主要特点是数据的单向传递,由实参通过形参将数据代入被调用函数,不论在调用期间形参值是否改变,调用结束返回主调函数之后,实参值都不会改变。在不同的算法语言中,传址调用的语法有所不同。在PASCAL语言中用变参实现传址。在C语言中采用指针变量做形参来实现传址。传址调用的主要特点是可以实现数据双向传递,在调用时实参将地址传给形参,该地址中的数据代入被调用函数。如果在调用期间形参值被改变,也即该地址中的数据发生变化,调用结束返回主调函数之后,实参地址仍然不变,但是该地址中的数据发生相应改变。这就是数据的双向传递。现看一例题:例1.3 设计一个函数实现两个数据的交换,在主程序中调用。#include viod swap( int *a, int *b) ; /* 函数原型声明 */void main( ){ int x=100, y=800;printf(“\n %6d%6d”, x, y); /* 输出原始数据 */swap(&x, &y); /* 调用交换数据的函数swap() */printf(“\n %6d%6d”, x ,y); /* 输出交换后的数据 */}viod swap( int *a, int *b){ int c;c=*a; *a = *b; *b=c;}运行结果:800800 100实践证明x,y 的数据在调用函数前后发生了交换变化。形参是指向整形的指针变量a和b,在函数体内需要交换的是指针所指的存储单元的内容,因此使用*a = *b;这样的写法。在调用时,要求实参个数、类型位置与形参一致。因为实参应该是指针地址,所以调用语句swap(&x, &y)中,实参&x,和& y代入的是整型变量x,y的地址。在函数体内交换的是实参地址中的内容,而作为主函数变量x,y的地址仍然没有改变。从整数交换的角度看,本例题实现了双向数据传递。若从指针地址角度看,调用前后指针地址不变。现在回过头来看P5页[复数ADT实现的面向过程C语言源程序]的创建复数的函数:void creat(complex *c){ …….; c->x=x1; c->y=y1;}在函数体中人们容易认识和习惯的写法c.x和c.y,也必须写成c->x和c->y。在调用该函数时,还必须将结构体变量a求地址做实参:creat(&a)。初学者应该特别注意这一点。如果需要在函数体中改变指针的地址,这就需要在原指针基础之上再加一级指针:void funz( int **a){ /* 改变*a */ …}函数调用返回后**a仍然不变,而*a发生了变化。由此可以看出C语言的传址调用比较复杂。不如PASCAL的变量参数简便,也不如C++的引用调用方便。三、 结构体及运用数据结构课程所研究的问题均运用到“结构体”。在C语言中结构体的定义、输入/输出是数据结构程序设计的重要语法基础。定义结构体的一般格式:struct 结构体类型名{ 类型名1 变量名1; //数据子域类型名2 变量名2;……类型名n 变量名n;};其中struct是保留字。结构体类型名由用户自己命名。在使用时必须声明一个具体的结构体类型的变量,声明创建一个结构体变量的方法是:struct 结构体类型名 结构体变量名;例如: struct ElemType /* 定义结构体 */{ int num; char name[10];} ;struct ElemType x; /* 声明结构体变量x */另外有一种方法使用typedef 语句定义结构体,在声明结构体变量时可以不写struct,使得书写更加简便。例如:typedef struct{ int num;char name[10];} ElemType;ElemType就是一个新的类型名,并且是结构体类型名。声明变量x的语句是:ElemType x;一个结构体中可以包含多个数据子域。数据子域的类型名一般指基本数据类型(int char 等),也可是已经定义的另一结构体名。数据子域变量名可以是简单变量,也可以是数组。它们也可以称为结构体的数据成员。通过“结构体变量名.数据子域” 可以访问数据子域。例1.6 设计Student结构体,在主程序中运用。#include #include typedef struct /* 定义结构体Student */{ long num; /* 学号 */int x; /* 成绩 */char name[10]; /* 姓名 */} Student;void main( ){ Student s1; /* 声明创建一个结构体变量s1 */s1.num=1001 ; /* 为s1的数据子域提供数据 */s1. x=83;strcpy( s1.name, “ 李 明”);printf( “\n 姓名: %s”, s1.name); /* 输出结构体变量s1 的内容 */printf( “\n 学号: %d”, s1.num);printf( “\n 成绩: %d”, s1.x);}或者使用键盘输入:{ scanf(“%d”, s1.num);scanf(“%d”, s1.x);scanf(“%s”, s1.name);}还可以通过“结构体指针->数据子域” 来访问数据域。在实际问题中还会使用到指向结构体的指针,通过以下语句段可以说明结构体指针的一般用法。{ Student *p; /* 声明指针变量p */p=( Student *)malloc(sizeof( Student)); /* 分配存储单元,首地址赋给p指针 */p->num=101; p->x=83; strcpy( p->name, “李 明 ”);printf(“\n %10s%6d%6d”,p->name,p->num,p->x);}设计一个一维数组,每个数组元素是Student结构体类型,通过以下语句段可以说明结构体数组的一般用法。可以通过“结构体数组名[下标].数据子域”访问数据域。{ Student a[5]; /* 声明创建一个结构体数组a */int i ;for( i=0, i<5, i++){printf(“\n 学号:%d”,a[i].num) ;printf(“\n 姓名:%s”,a[i].name) ;printf(“\n 成绩:%d”,a[i].x) ;}}以上是关于结构体的基本概念和简单运用。第二部分 上机实验习题上机实验要求及规范数据结构课程具有比较强的理论性,同时也具有较强的可应用性和实践性。在上机实验是一个重要的教学环节。一般情况下学生能够重视实验环节,对于编写程序上机练习具有一定的积极性。但是容易忽略实验的总结,忽略实验报告的撰写。对于一名大学生必须严格训练分析总结能力、书面表达能力。需要逐步培养书写科学实验报告以及科技论文的能力。拿到一个题目,一般不要急于编程。按照面向过程的程序设计思路(关于面向对象的训练将在其它后继课程中进行),正确的方法是:首先理解问题,明确给定的条件和要求解决的问题,然后按照自顶向下,逐步求精,分而治之的策略,逐一地解决子问题。具体实习步骤如下:1.问题分析与系统结构设计充分地分析和理解问题本身,弄清要求做什么(而不是怎么做),限制条件是什么。按照以数据结构为中心的原则划分模块,搞清数据的逻辑结构(是线性表还是树、图?),确定数据的存储结构(是顺序结构还是链表结构?)。然后设计有关操作的函数。在每个函数模块中,要综合考虑系统功能,使系统结构清晰、合理、简单和易于调试。最后写出每个模块的算法头和规格说明,列出模块之间的调用关系(可以用图表示),便完成了系统结构设计。2.详细设计和编码详细设计是对函数(模块)的进一步求精,用伪高级语言(如类C语言)或自然语言写出算法框架,这时不必确定很多结构和变量。编码,即程序设计,是对详细设计结果的进一步求精,即用某种高级语言(如C/C++语言)表达出来。尽量多设一些注释语句,清晰易懂。尽量临时增加一些输出语句,便于差错矫正,在程序成功后再删去它们。3.上机准备熟悉高级语言用法,如C语言。熟悉机器(即操作系统),基本的常用命令。静态检查主要有两条路径,一是用一组测试数据手工执行程序(或分模块进行);二是通过阅读或给别人讲解自己的程序而深入全面地理解程序逻辑,在这个过程中再加入一些注释和断言。如果程序中逻辑概念清楚,后者将比前者有效。4.上机调试程序调试最好分块进行,自底向上,即先调试底层函数,必要时可以另写一个调用驱动程序,表面上的麻烦工作可以大大降低调试时所面临的复杂性,提高工作效率。5.整理实习报告在上机实开始之前要充分准备实验数据,在上机实践过程中要及时记录实验数据,在上机实践完成之后必须及时总结分析。写出实验报告。一、实验报告的基本要求:一般性、较小规模的上机实验题,必须遵循下列要求。养成良好的习惯。姓名 班级 学号 日期题目:内容叙述程序清单(带有必要的注释)调试报告:实验者必须重视这一环节,否则等同于没有完成实验任务。这里可以体现个人特色、或创造性思维。具体内容包括:测试数据与运行记录;调试中遇到的主要问题,自己是如何解决的;经验和体会等。二、实验习报告的提高要求:阶段性、较大规模的上机实验题,应该遵循下列要求。养成科学的习惯。需求和规格说明描述问题,简述题目要解决的问题是什么。规定软件做什么。原题条件不足时补全。设计设计思想:存储结构(题目中限定的要描述);主要算法基本思想。设计表示:每个函数的头和规格说明;列出每个函数所调用和被调用的函数,也可以通过调用关系图表达。实现注释:各项功能的实现程度、在完成基本要求的基础上还有什么功能。用户手册:即使用说明。调试报告:调试过程中遇到的主要问题是如何解决的;设计的回顾、讨论和分析;时间复杂度、空间复杂度分析;改进设想;经验和体会等。实习一 复数ADT及其实现一、实验目的1. 了解抽象数据类型(ADT)的基本概念,及描述方法。2. 通过对复数抽象数据类型ADT的实现,熟悉C语言语法及程序设计。为以后章节的学习打下基础。二、实例复数抽象数据类型ADT的描述及实现。[复数ADT的描述]ADT complex{数据对象:D={ c1,c2 c1,c2∈FloatSet }数据关系:R={ c1 c2 }基本操作:创建一个复数 creat(a);输出一个复数 outputc(a);求两个复数相加之和 add(a,b);求两个复数相减之差 sub(a,b);求两个复数相乘之积 chengji(a,b);等等;} ADT complex;[复数ADT实现的源程序]#include #include /* 存储表示,结构体类型的定义 */typedef struct{ float x; /* 实部子域 */float y; /* 虚部的实系数子域 */}comp;/* 全局变量的说明 */comp a,b,a1,b1;int z;/* 子函数的原型声明 */void creat(comp *c);void outputc(comp a);comp add(comp k,comp h);/* 主函数 */main(){ creat(&a); outputc(a);creat(&b); outputc(b);a1=add(a,b); outputc(a1);} /* maijn *//* 创建一个复数 */void creat(comp *c){ float c1,c2;printf("输入实部real x= ");scanf("%f",&c1);printf("输入虚部xvpu y= ");scanf("%f",&c2);(*c).x=c1; c ->y=c2;} /* creat *//* 输出一个复数 */void outputc(comp a){ printf("\n %f+%f i \n\n",a.x,a.y);}/* 求两个复数相加之和 */comp add(comp k,comp h){ comp l;l.x=k.x+h.x; l.y=k.y+h.y;return(l);} /* add */三、实习题首先将上面源程序输入计算机,进行调试。运行程序,输入下列两个复数的实部域虚部,记录两个复数相加的输出结果。 原始数据:2.0 + 3.5i ,3.0 – 6.3i然后在上面程序的基础上,增加自行设计的复数减、复数乘的两个子函数,适当补充必需的语句(例如函数原型声明、主函数中的调用等)。提示:// 求两个复数相减之差的函数comp sub(comp k,comp h) { ……}// 求两个复数相乘之积的函数comp chengji(comp k,comp h){ …… }再次调试运行程序。输入数据,记录结果,最后完成实验报告。实习二 线性表一、实验目的1. 了解线性表的逻辑结构特性,以及这种特性在计算机内的两种存储结构。2. 重点是线性表的基本操作在两种存储结构上的实现;其中以链表的操作为侧重点;并进一步学习结构化的程序设计方法。二、实例1. 线性表的顺序存储表示(结构)及实现。阅读下列程序请注意几个问题:(1)关于线性表的顺序存储结构的本质是:在逻辑上相邻的两个数据元素ai-1, ai,在存储地址中也是相邻的,既地址连续。不同的教材有不同的表示,有的直接采用一维数组,这种方法有些过时。有的采用含‘动态分配’一维数组的结构体,这种方法过于灵活抽象(对读者要求过高)。我们采用的是含‘静态’一维数组和线性表长的结构体:typedef struct{ ElemType a[MAXSIZE]; /* 一维数组 子域 */int length; /* 表长度子域 */}SqList; /* 顺序存储的结构体类型 */(2)本程序是一个完整的、子函数较多的源程序。目的为学生提供一个示范,提供顺序存储表示的资料,供学生参考。比如,主函数中简单“菜单设计”(do-while循环内嵌套一个 switch结构)技术。在学习数据结构的初级阶段,并不强要求学生一定使用“菜单设计”技术,同学们可以在main()函数中直接写几个简单的调用语句,就象前面的复数处理程序中的main()一样。但是随着学习的深入,尽早学会使用“菜单设计”技术,会明显提高编程和运行效率。[源程序]#include #include #define MAXSIZE 20 /* 数组最大界限 */typedef int ElemType; /* 数据元素类型 */typedef struct{ ElemType a[MAXSIZE]; /* 一维数组 子域 */int length; /* 表长度子域 */}SqList; /* 顺序存储的结构体类型 */SqList a,b,c;/* 函数声明 */void creat_list(SqList *L);void out_list(SqList L);void insert_sq(SqList *L,int i,ElemType e);ElemType delete_sq(SqList *L,int i);int locat_sq(SqList L,ElemType e);/* 主函数 */main(){ int i,k,loc; ElemType e,x; char ch;do { printf("\n\n\n");printf("\n 1. 建立线性表 " );printf("\n 2. 在i位置插入元素e");printf("\n 3. 删除第i个元素,返回其值");printf("\n 4. 查找值为 e 的元素");printf("\n 6. 结束程序运行");printf("\n======================================");printf("\n 请输入您的选择(1,2,3,4,6)");scanf("%d",&k);switch(k){ case 1:{ creat_list(&a); out_list(a);} break;case 2:{ printf("\n i,e= "); scanf("%d,%d",&i,&e);insert_sq(&a,i,e); out_list(a);} break;case 3:{ printf("\n i= "); scanf("%d",&i);x=delete_sq(&a,i); out_list(a);printf("\n x=%d",x);} break;case 4:{ printf("\n e= "); scanf("%d",&e);loc=locat_sq(a,e);if (loc==-1) printf("\n 未找到 %d",loc);else printf("\n 已找到,元素位置是 %d",loc);} break;} /* switch */}while(k!=6);printf("\n 再见!");printf(“\n 打回车键,返回。“); ch=getch();} /* main *//* 建立线性表 */void creat_list(SqList *L){ int i;printf("\n n= "); scanf("%d",&L->length);for(i=0;ilength;i++){ printf("\n data %d= ",i);scanf("%d",&(L->a[i]));}} /* creat_list *//* 输出线性表 */void out_list(SqList L){ int i; char ch;printf("\n");for(i=0;i<=L.length-1;i++) printf("%10d",L.a[i]);printf("\n\n 打回车键,继续。“); ch=getch();} /* out_list *//* 在线性表的第i个位置插入元素e */void insert_sq(SqList *L,int i,ElemType e){ int j;if (L->length==MAXSIZE) printf("\n overflow !");else if(i<1||i>L->length+1) printf("\n erroe i !");else { for(j=L->length-1; j>i-1; j--) L->a[j+1]=L->a[j];/* 向后移动数据元素 */L->a[i-1]=e; /* 插入元素 */L->length++; /* 线性表长加1 */}} /* insert_sq *//* 删除第i个元素,返回其值 */ElemType delete_sq(SqList *L, int i){ ElemType x; int j;if( L->length==0) printf("\n 是空表。underflow !");else if(i<1||i> L->length){ printf("\n error i !");x=-1;}else { x=L->a[i-1];for(j=i; j<=L->length-1; j++) L->a[j-1]=L->a[j];L->length--;}return(x);} /* delete_sq *//* 查找值为 e 的元素,返回它的位置 */int locat_sq(SqList L, ElemType e){ int i=0;while(i<=L.length-1 && L.a[i]!=e) i++;if(i<=L.length-1) return(i+1);else return(-1);}/* locat_sq */2. 线性表的链表存储表示(结构)及实现。阅读下列程序请注意几个问题:(1)关于线性表的链表存储结构的本质是:在逻辑上相邻的两个数据元素ai-1, ai,在存储地址中可以不相邻,既地址不连续。不同的教材的表示基本是一致的。typedef struct LNode{ ElemType data; /* 数据子域 */struct LNode *next; /* 指针子域 */}LNode; /* 结点结构类型 */(2)本程序是一个完整的、子函数较多的源程序。目的为学生提供一个示范,提供关于链表操作的资料,供学生参考。可以看到本程序的main()与前一程序的main()函数的结构十分相似。稍加改动还可为其他题目的源程序所用。[源程序]#include #include #include typedef int ElemType;typedef struct LNode{ ElemType data; /* 数据子域 */struct LNode *next; /* 指针子域 */}LNode; /* 结点结构类型 */LNode *L;/* 函数声明 */LNode *creat_L();void out_L(LNode *L);void insert_L(LNode *L,int i ,ElemType e);ElemType delete_L(LNode *L,int i);int locat_L(LNode *L,ElemType e);/* 主函数 */main(){ int i,k,loc; ElemType e,x; char ch;do { printf("\n\n\n");printf("\n\n 1. 建立线性链表 ");printf("\n\n 2. 在i位置插入元素e");printf("\n\n 3. 删除第i个元素,返回其值");printf("\n\n 4. 查找值为 e 的元素");printf("\n\n 5. 结束程序运行");printf("\n======================================");printf("\n 请输入您的选择 (1,2,3,4,5)"); scanf("%d",&k);switch(k){ case 1:{ L=creat_L( ); out_L(L);} break;case 2:{ printf("\n i,e= "); scanf("%d,%d",&i,&e);insert_L(L,i,e); out_L(L);} break;case 3:{ printf("\n i= "); scanf("%d",&i);x=delete_L(L,i); out_L(L);if(x!=-1) printf("\n x=%d\n",x);} break;case 4:{ printf("\n e= "); scanf("%d",&e);loc=locat_L(L,e);if (loc==-1) printf("\n 未找到 %d",loc);else printf("\n 已找到,元素位置是 %d",loc);} break;} /* switch */printf("\n ----------------");}while(k>=1 && k<5);printf("\n 再见!");printf(“\n 打回车键,返回。“); ch=getch();} /* main *//* 建立线性链表 ( 11,22 ,33 ) */LNode *creat( ){ LNode *h,*p,*s; ElemType x;h=(LNode *)malloc(sizeof(LNode)); /* 分配头结点 */h->next=NULL;p=h;printf("\n data= "); scanf("%d",&x); /* 输入第一个数据元素 */while( x!=-111) /* 输入-111,结束循环 */{ s=(LNode *)malloc(sizeof(LNode)); /* 分配新结点 */s->data=x; s->next=NULL;p->next=s; p=s;printf("data= ( -111 end) "); scanf("%d",&x); /* 输入下一个数据*/}return(h);} /* creat_L *//* 输出单链表中的数据元素*/void out_L(LNode *L){ LNode *p; char ch;p=L->next; printf("\n\n");while(p!=NULL) { printf("%5d",p->data); p=p->next;};printf("\n\n 打回车键,继续。“); ch=getch();} /* out_link *//* 在i位置插入元素e */void insert_L(LNode *L,int i, ElemType e){ LNode *s,*p,*q; int j;p=L; /* 找第i-1个结点 */j=0;while(p!=NULL && jnext; j++; }if(p==NULL || j>i-1) printf("\n i ERROR !");else { s=(LNode *)malloc(sizeof(LNode));s->data=e;s->next=p->next;p->next=s;}} /* insert_L *//* 删除第i个元素,返回其值 */ElemType delete_L(LNode *L,int i){ LNode *p,*q; int j; ElemType x;p=L; j=0;while(p->next!=NULL && jnext; j++;}if(p->next==NULL) {printf("\n i ERROR !"); return(-1);}else { q=p->next; x=q->data;p->next=q->next; free(q);return(x);}} /* delete_L *//* 查找值为 e 的元素, 返回它的位置 */int locat_L(LNode *L,ElemType e){ LNode *p; int j=1;p=L->next;while(p!=NULL && p->data!=e) {p=p->next; j++;}if(p!=NULL)return(j); else return(-1);} /* locat_L */3.约瑟夫问题的一种描述:编号为1,2,……,n的n个人按顺时针方向围坐一圈,每人持有一个密码(正整数)。一开始任选一个正整数作为报数的上限值m,从第一个人开始按顺时针方向自1开始顺序报数。方法1.报m的人出列(将其删除),从他在顺时针方向上的下一个人开始重新从一报数,……,如此下去,直到所有人全部出列为止。试设计一个程序求出出列顺序。要求利用单向循环链表存储结构模拟此过程,按照出列的顺序打印出各人的编号和此人密码。方法2. 报m的人出列(将其删除),将他的密码作为新的m值,从他在顺时针方向上的下一个人开始重新从一报数,……,如此下去,直到所有人全部出列为止。试设计一个程序求出出列顺序。要求利用单向循环链表存储结构模拟此过程,按照出列的顺序打印出各人的编号和此人密码。[方法1的程序清单]#include #include typedef struct node /* 结点的结构定义 */{ int num; /* 编号子域 */int sm; /* 密码子域 */struct node *next; /* 指针域 */}JOS;/* 函数声明 */JOS *creat();void outs(JOS *h, int m);/* 主函数 */main(){ int m; JOS *h;h=creat();printf(“\n enter the begin secret code, m=(m>1)”); scanf(“%d”,&m);outs(h, m);} /* main *//* 生成约瑟夫环 */JOS *creat(){ int i=0, mi;JOS *new, *pre, *h;h=(JOS *)malloc(sizeof(JOS)); /* 为头结点申请空间 */h->link=h; /* 头结点h形成环 */pre=h;printf(“\n 个人密码 = ”); scanf(“%d”,&mi);while( mi != -111) /* 密码为-111,结束循环 */{ new=(JOS *)malloc(sizeof(JOS)); /* 申请数据元素结点空间 */i++; new->num=i; new->sm=mi; /* 结点送值 */new->link=h;pre->link=new;pre=new; /* 形成循环链表 */printf(“\n 个人密码 = ”); scanf(“%d”,&mi); /* 读入下一个密码 */} /* while */pre->link= h->link; free(h); /* 删除头结点h */h=pre->link; return(h); /* 头指针指向第一个数据元素结点 */} /* JOS *creat , 该约瑟夫环无附加头结点 *//* 按m被叫出列的顺序,输出结点信息 */void outs(JOS *h, int m){ int i; JOS *q=h, p;printf(“\n “);while (q->link!=q){ for(i=1;ilink;} /* 报数到第m个人 */printf(“%6d %6d ”,q->num,q->sm); /* 输出第m个人的编号和密码 */p->link=q->link; free(q); /* 第m个人出列 */q=p->link;}printf(“%6d %6d \n”,q->num,q->sm); /* 输出最后一个结点的编号值 */free(q);} /* outs */本程序用了不带头结点的循环链表,也可以加上头结点,对于本题有头结点会使操作麻烦,(同学们可以试一试,加进头结点如何实现?)并不是任何时候有头结点都能使程序操作简化,要根据实际情况,决定用是否使用头结点。感兴趣的同学可以设计一个程序实现约瑟夫环的方法2。在一般情况下默认使用头结点。不论是单向链表还是循环链表,头指针不能丢。三、实习题1. 用顺序存储表示(数组)实现约瑟夫环问题。2. 一个线性表有n个元素(n3. 从单链表中删除指定的元素x,若x在单链表中不存在,给出提示信息。要求: (1)指定的值x由键盘输入;(2)程序能处理空链表的情况。4.用链表建立通讯录。通讯录内容有:姓名、通讯地址、电话号码。要求:(1)通讯录是按姓名项的字母顺序排列的;(2)能查找通讯录中某人的信息;[提示] 可用链表来存放这个通讯录,一个人的信息作为一个结点。成链的过程可以这样考虑:先把头结点后面的第一个数据元素结点作为链中的首结点,也是末结点。从第二个数据开始逐一作为‘工作结点’,需从链表的首结点开始比较,如果‘工作结点’的数据比链中的‘当前结点’的数据小,就插在其前面。否则,再看后面是否还有结点,若没有结点了就插在其后面成为末结点;若后面还有结点,再与后面的结点逐一比较处理。5. 超长正整数的加法,设计一个程序实现两个任意长的整数求和运算[提示] 可采用一个带有头结点的循环链表来表示一个非负的超大整数。从低位 开始每四位组成的数字,依次放在链表的第一个、第二个、……结点中,不足四位的最高位存放在链表的最后一个结点中,表头结点值规定位-1。例如:大整数“567890987654321”可用如下的头结点的链表表示:按照此数据结构,可以从两个表头结点开始,顺序依次对应相加,求出所需要的进位后,将其代入下一个结点进行运算。实习三 栈和队列一、实习目的掌握栈这种数据结构特性及其主要存储结构,并能在现实生活中灵活运用。掌握队列这种数据结构特性及其主要存储结构,并能在现实生活中灵活运用。了解和掌握递归程序设计的基本原理和方法。二、实例在各种教科书中关于栈和队列叙述十分清晰。但是,它们在计算机内的实现介绍不够详细。为了减轻学生上机实验的困难,在此给出几个例题供参考。栈的顺序存储结构及实现。#include #include #define MAXSIZE 20 /* 数组最大界限 */typedef int ElemType; /* 数据元素类型 */typedef struct{ ElemType a[MAXSIZE]; /* 一维数组子域 */int top; /* 栈顶指针子域 */}SqStack; /* 栈的顺序结构体类型 */SqStack s1;/* 函数声明 */void init_s(SqStack *s);void out_s(SqStack s);void push(SqStack *s,ElemType e);ElemType pop(SqStack *s);/* 主函数 */main(){ int k; ElemType e,x; char ch;init_s( &s1); /* 初始化一个空栈 */do { printf("\n\n\n");printf("\n\n 1. 数据元素e进栈 ");printf("\n\n 2. 出栈一个元素,返回其值");printf("\n\n 3. 结束程序运行");printf("\n======================================");printf("\n 请输入您的选择 (1,2,3)");scanf("%d",&k);switch(k){ case 1:{ printf("\n 进栈 e= "); scanf("%d",&e);push( &s1,e); out_s( s1 );} break;case 2:{ x= pop( &s1);printf("\n出栈元素 : %d", x);out_s( s1 );} break;case 3: exit(0);} /* switch */printf("\n ----------------");}while(k>=1 && k<3);printf("\n 再见!")printf(“\n 打回车键,返回。“); ch=getch();} /* main *//* 初始化空栈 * /void init_s(SqStack *s){ s->top=-1;} /* init_s *//* 输出栈的内容 */void out_s(SqStack s){ char ch; int i; /* 千万不能修改栈顶指针top */if (s->top==-1) printf(“\n Stack is NULL. “);else{ i=s->top;while( i!=-1){ printf(“\n data=%d”, s->a[i]);i--; }}printf(“\n 打回车键,继续。“); ch=getch();} /* out_c *//* 进栈函数 */void push(SqStack *s,ElemType e){ if(s->top==MAXSIZE-1)printf(“\n Sstack is Overflow!”);else{ s->top++ ;s->a[s->top]=e;}}/* push *//* 出栈函数 */ElemType pop(SqStack *s){ ElemType x;if(s->top==-1){ printf(“\n Stack is Underflow!”);x=-1; }else { x=s->a[s->top];s->top--; }return(x);} /* pop */循环队列(即队列的顺序存储结构)实现。#include #include #define MAXSIZE 20 /* 数组最大界限 */typedef int ElemType; /* 数据元素类型 */typedef struct{ ElemType a[MAXSIZE]; /* 一维数组子域 */int front,rear; /* 头、尾指针子域 */}SqQueue; /* 循环队列的结构体类型 */SqQueue Q1;/* 函数声明 */void init_Q(SqQueue *Q);void out_Q(SqQueue Q);void EnQueue(SqQueue *Q,ElemType e);ElemType DeQueue(SqQueue *Q);/* 主函数 */main(){ int k; ElemType e,x; char ch;init_Q( &Q1); /* 初始化一个空循环队列 */do { printf("\n\n\n");printf("\n\n 1. 数据元素e进队列 ");printf("\n\n 2. 出队一个元素,返回其值");printf("\n\n 3. 结束程序运行");printf("\n======================================");printf("\n 请输入您的选择 (1,2,3)");scanf("%d",&k);switch(k){ case 1:{ printf("\n 进队 e= "); scanf("%d",&e);EnQueue(SqQueue &Q1,e); out_Q(Q1);} break;case 2:{ x= DeQueue(&Q1);printf("\n出队元素 : %d", x);out_Q(Q1 );} break;case 3: exit(0);} /* switch */printf("\n ----------------");}while(k>=1 && k<3);printf("\n 再见!");printf(“\n 打回车键,返回。“); ch=getch();} /* main *//* 初始化空队列 * /void init_Q(SqQueue *Q){ Q->front=0; Q->rear=0;} /* init_Q *//* 输出队列的内容 */void out_Q(SqQueue Q){ char ch; int i;/* 不能修改队列头、尾指针 */if (Q->front==Q->rear) printf(“\n Queue is NULL. “);else{ i=(Q->front+1)% MAXSIZE;while( i!=Q->rear){ printf(“\n data=%d”, Q->a[i]);i=(i+1)%MAXSIZE; }printf(“\n data=%d”, Q->a[i]);}printf(“\n 打回车键,继续。“); ch=getch();} /* out_Q *// * 进队函数 */void EnQueue(SqQueue *Q,ElemType e){ if((Q->rear+1)%MAXSIZE==Q->front) printf(“\n Queue is Overflow!”);else{ Q->rear=(Q->rear+1)% MAXSIZE ;Q->a[Q->rear]=e;}}/* EnQueue *//* 出队函数 */ElemType DeQueue(SqQueue *Q){ ElemType x;if(Q->front==Q->rear){ printf(“\n Queue is NULL!”);x=-1;}else { Q->front=(Q->front+1)% MAXSIZE ;x=Q->a[Q->front];}return(x);} /* DeQueue */队列的链表储结构及实现。#include #include #include typedef int ElemType;typedef struct QNode{ ElemType data; /* 数据子域 */struct QNode *next; /* 指针子域 */}QNode; /* 结点结构类型 */typedef struct{ Qnode *front, *rear; /* 头、尾指针子域 */}L_Queue; /* “头尾”结点结构类型 */L_Queue Q1;/* 函数声明 */void init_Q(L_Queue *Q);void out_Q(L_Queue Q);void EnQueue(L_Queue Q,ElemType e);ElemType DeQueue(L_Queue Q);/* 主函数 */main(){ int k; ElemType e,x; char ch;init_Q( &Q1); /* 初始化一个空循环队列 */do { printf("\n\n\n");printf("\n\n 1. 数据元素e进队列 ");printf("\n\n 2. 出队一个元素,返回其值");printf("\n\n 3. 结束程序运行");printf("\n======================================");printf("\n 请输入您的选择 (1,2,3)");scanf("%d",&k);switch(k){ case 1:{ printf("\n 进队 e= "); scanf("%d",&e);EnQueue(SqQueue &Q1,e); out_Q(Q1);} break;case 2:{ x= DeQueue(&Q1);printf("\n出队元素 : %d", x);out_Q(Q1 );} break;case 3: exit(0);} /* switch */printf("\n ----------------");}while(k>=1 && k<3);printf("\n 再见!");printf(“\n 打回车键,返回。“); ch=getch();} /* main *//* 初始化一个空队列 */void init_Q(L_Queue *Q){ QNode *p ;p=(QNode *)malloc(sizeof(QNode));p->next=NULL;Q->fornt=p; Q->rear=p;} /* init_Q *//* 输出队列的内容 */void out_Q(L_Queue Q){ QNode *p; char ch;p=Q.front->next;while(p!=NULL) { printf(“\n %d”,p->data);p=p->next;}printf(“\n 打回车键,继续。“); ch=getch();} /* out_Q *// * 进队函数 */void EnQueue(L_Queue Q,ElemType e){ QNode p,s;s=(QNode *)malloc(sizeof(QNode));s->data=e; s->next=NULL;Q.rear->next=s; Q.rear=s;} /* EnQueue *//* 出队函数 */ElemType DeQueue(L_Queue Q){ ElemType x; QNode *p;if(Q.front==Q.rear) { printf(“\n Queue is NULL!”);x=-1;}else { p=Q.front->next;x=p->data;Q.front->next=p->next;If(Q.rear= =p) Q.rear=Q.fornt;Free(p);}return(x);} /* DeQueue */三、实习题写一个程序,将输入的十进制数据M 转换为八进制数据M8,将其调试通过。在此基础上修改程序,实现十进制数据M 向N 进制(2或8或16)的转换。(1)采用顺序存储结构实现栈。(2)采用链表结构实现栈。2. 阿克曼函数(Ackermann’s function)定义如下:人们之所以研究该函数,是因为m和n值的较小增长都会引起函数值的极快增长。(1)设计运用递归算法的源程序,上机运行。(2)写一个非递归算法的源程序,上机运行。并进行比较。3.二项式(a+b)n展开后,其系数构成杨辉三角形,利用队列写出打印杨辉三角形的前n行的程序。实习四 串一、实习目的1. 熟悉串类型的实现方法,了解简单文字处理的设计方法。2. 熟悉C语言的字符和把字符串处理的原理和方法。下面简单介绍C相关知识:(1) 字符:char ch; ch是单个字符变量ch=’a’; ’a’ 是字符常量(2)字符串:Char s1[10],*s2,s3[2][10];S1 是一维字符数组,也称它是字符串变量;S2 是指向字符的指针变量,可以给它分配一批单元存放字符,也称为串;S2=(char )malloc(sizeof(char)*10);S3 是2行10列的二维字符数组,一般看做含有2个字符串的数组s3[0],s3[1],每个串最多容10个字符;下列赋值语句是错误的:S1= “abcdefghi” ;应该使用串拷贝函数:strcpy(S1, “abcdefghi”); strcoy( S2, ”abcdefghi”);strcpy(S3[0], ”123456789”); strcpy(S3[1], ”jklmnopqr”) ;用双引号括起来的是字符串常量,在处理字符串常量时,注意总是有一个字符‘\0’作为字符串常量的结尾 。”123456789”存入S3[0]要占用10个位置,而不是9个。S3[0][0]里是1,……,s3[0][8]里是9,s3[0][9]里是‘\0’(串尾标志)。(3)常见的字符函数:int strlen(streing)string 可以是串变量或常量, 函数结果是串长度(不含‘\0’)。strcpy( str ,str2 );str是串变量,str2可以是串变量或常量。函数的作用是将str2的串拷贝给串变量str。 int strcmp( str1 ,str2 );str1,str2可以是串变量或常量,函数的作用是将两个串进行比较,当 str1==str2,函数结果=0;当 str1>str2, 函数结果>0;当 str1char *strcat( str1,str2);str1,str2可以是串变量或常量,函数的作用是将两个串进行连接,函数结果是连接后的新串。二、实例实现字符串置换操作。#include #include /* 函数声明 */int index(char *s, char *t,int pos);void replace(char *s,char *t,char *v);/* 主函数 */main(){ int k; char *s,t[]="abc",*v="%%%";s=(char *)malloc(100*sizeof(char));printf("\n s= "); gets(s); printf("\n s=%s\n",s);printf("\n t=%s v=%s\n",t,v);replace(s,t,v); /* 调用串置换函数 */printf("\n\n new string=%s\n",s);} /* main *//* 串的置换,将主串s中的t串,置换为v串 */void replace(char *s,char *t,char *v){ int i,j,k,po,sl,tl;sl=strlen(s); tl=strlen(t);printf("\n sl=%d tl=%d\n",sl,tl);po=0;while( po< sl-tl+1){ k=index(s,t,po); /* 调用串匹配函数 */printf("\n k=%2d",k);i=k-1;for(j=0;j<=tl-1;j++) { s[i]=v[j];i++;}po=k+tl;printf(" pos=%2d",po);}} /* replace *//* 串匹配函数 *//* 从主串s的第pos个字符开始查找子串t,函数结果是子串t在主串s的pos开始之后首次出现的位置 */int index(char *s, char *t, int pos){int i,j,sl,tl;i=pos; j=1; sl=strlen(s); tl=strlen(t);while(i<=sl && j<=tl)if(s[i-1]==t[j-1]) {i++; j++; }else { i=i-j+1+1; j=1;}if(j>tl) return(i-tl);else return(-1);} /* index */三、实习题将上述实例打入计算机,调试运行。字符串常量的提供有多种手段。可以在定义串类型同时初始化串值:char t[]="abc",*v="%%%";还可以使用语句gets(s)进行输入;也可以用scanf("%s",s)进行输入。请你试着用不同的方法修改、调试、运行程序。设计可以在主串s中第i个位置插入一个子串t的程序。设计可以在主串s中从第i个位置开始共取m个字符,求子串的程序。实习五 数 组一、实习目的熟悉稀疏矩阵的“三元组表”和“十字链表”存储结构,运用它们进行矩阵简单运算处理。二、实例稀疏矩阵的十字链表存储与输出。#include #include typedef int Etype;typedef struct OLnode{int i,j; /* 行号、列号域 */Etype e; /* 数据域 */struct OLnode *right,*down; /* 行向的、列向的指针域 */} OLnode; /* 数据元素结点类型 */typedef struct{ OLnode *rh[5],*ch[5];int mu,nu,tu;}Crosslist; /* 十字链表行、列表头 *//* 函数声明 */void creatMatrix(Crosslist *M);void out_M(Crosslist M);Crosslist ma; int z;/* 主函数 */main(){ creatMatrix(&ma);out_M(ma);} /* main *//* 十字链表的输出 */void out_M(Crosslist M){ int i; OLnode *p; char ch;/* 输出矩阵的总行数、总列数、非零元素总个数 */printf("\n m=%d n=%d t=%d\n",M.mu,M.nu,M.tu);for(i=1; i<=M.mu; i++){ p=M.rh[i]; /* 指向第i行 */if(p){ printf("\n i=%d",i);while(p){ printf(" (%3d%3d%4d) ",p->i,p->j,p->e);p=p->right;}}printf("\n");}printf(“\n 打回车键,返回。“); ch=getch();}void creatMatrix(Crosslist *M){ int m,n,t,row,col,i,j;Etype va; OLnode *p,*q,*s;/* 输入矩阵的总行数、总列数、非零元素总个数 */printf("\n m,n,t= "); scanf("%d,%d,%d",&m,&n,&t);for(i=1; i<=m;i++) M->rh[i]=NULL;for(j=1; j<=n;j++) M->ch[j]=NULL;M->mu=m; M->nu=n; M->tu=t; /* 建立成空十字链表 *//* 以下为非零元素的逐一输入和插入 */for(i=1;i<=M->tu;i++){ printf("\n i,j,e= "); scanf("%d,%d,%d",&row,&col,&va);p=(OLnode *)malloc(sizeof(OLnode));p->i=row; p->j=col; p->e=va;/* 在第row行上链接 */q=M->rh[row]; s=q;while(q!=NULL && q->j < col){ s=q; q=q->right;}p->right=q;if(q==M->rh[row])M->rh[row]=p; else s->right=p;/* 在第col列上链接 */q=M->ch[col];while(q && q->i < row){s=q;q=q->down;}p->down=q;if(q==M->ch[col]) M->ch[col]=p; else s->down=p;} /* for */}/* creatMatrix */三、实习题编写用“三元组表”存储(参见教科书P98页)稀疏矩阵,进行矩阵处理的程序。(1)矩阵转置 (2)矩阵加上机调试上面给出的十字链表的程序,在此基础上增加查找功能:给定一个数值 X ,查找确定它的位置(行下标i、列下标j);给定一个矩阵元素aij位置(i,j),求出它的数据值是什么?实现迷宫求解程序。实习六 树与二叉树一、实习目的熟练掌握二叉树在二叉链表存储结构中的常用遍历方法:先序递归遍历、中序递归和非递归遍历、后序递归遍历。了解二叉树的按层遍历、先序非递归遍历及后序递归遍历。用树解决实际问题,如哈夫曼编码等。加深对“数据结构+算法=程序”的理解和认识,提高编写较复杂程序的能力。二、实例1. 二叉树的建立和遍历。为了实现对二叉树的有关操作,首先要在计算机中建立所需的二叉树。建立二叉树有各种不同的方法。有一种方法利用二叉树的性质5来建立二叉树,输入数据时需要将结点的序号(按满二叉树编号)和数据同时给出:序号 数据元素。结合下图的二叉树数的输入据顺序应该是:1 1,2 2,3 3,4 4,6 5,7 6,11 7,12 8,13 9。另一种算法是教材P58~P59介绍的方法,这是一个递归方法,与先序遍历有点相似。数据的组织时先序的顺序,但是另有特点,当某结点的某孩子为空时以数据0来充当,也要输入。结合下图的二叉树数的输入据顺序应该是:1 2 4 0 0 0 3 5 0 7 0 0 6 8 0 0 9 0 0。若当前数据不为0,则申请一个结点存入当前数据。递归调用建立函数,建立当前结点的左右子树。下列程序中的统计二叉树结点的函数,是对二叉树遍历方法的应用,请认真理解然后模仿练习。/* 二叉树的建立与遍历 */# include # include typedef int Etype;typedef struct BiTNode /* 树结点结构 */{ Etype data;struct BiTNode *lch,*rch;}BiTNode;/* 函数原形声明 */BiTNode *creat_bt1();BiTNode *creat_bt2()void inorder(BiTNode *p);void numb(BiTNode *p);BiTNode *t; int n,n0,n1,n2,;/* 主函数 */main(){ char ch; int k;do { printf("\n\n\n");printf("\n\n 1. 建立二叉树方法1 ");printf("\n\n 2. 建立二叉树方法2");printf("\n\n 3. 中序递归遍历二叉树");printf("\n\n 4. 计算树中结点个数");printf("\n\n 5. 结束程序运行");printf("\n======================================");printf("\n 请输入您的选择 (1,2,3,4,5,6)"); scanf("%d",&k);switch(k){ case 1:t=creat_bt1( );break; /* 调用性质5建立二叉树算法 */case 2:t=creat_bt2( );break; /* 调用递归建立二叉树算法 */case 3: { inorder(t); /* 调用中序遍历 */printf("\n\n 打回车键,继续。“); ch=getch();} break;case 4:{ n=0;n0=0 ; n1=0; n2=0; /* 全局变量置0 */numb(t);printf(“\n 二叉树结点总数 n=%d”,n);printf(“\n 二叉树叶子结点数 n0=%d”,n0);printf(“\n 度为1的结点数 n1=%d”,n1);printf(“\n 度为2的结点数 n2=%d”,n2);printf("\n\n 打回车键,继续。“); ch=getch();} break;case 5: exit(0);} /* switch */printf("\n ----------------");}while(k>=1 && k<5);printf("\n 再见!");printf(“\n 打回车键,返回。“); ch=getch();} /* main *//* 利用二叉树性质5 ,借助一维数组V 建立二叉树 */BiTNode *creat_bt1(){ BiTNode *t,*p,*v[20]; int i; Etype e;/* 输入结点的序号i 、结点的数据e */printf("\n i,data= "); scanf("%d%d",&i,&e);while(i!=0 && e!=0) /* 当 i ,e都为0时,结束循环 */{ p=(BiTNode *)malloc(sizeof(BiTNode));p->data=e; p->lch=NULL; p->rch=NULL;v[i]=p;if (i==1) t=p; /* 序号为1的结点是根 */else{ j=i/2;if(i%2==0) v[j]->lch=p; /* 序号为偶数,做左孩子*/else v[j]->rch=p; /* 序号为奇数,做右孩子*/}printf("\n i,data= "); scanf("%d,%d",&i,&e);}return(t);} /* creat_bt1 *//* 模仿先序递归遍历方法,建立二叉树 */BiTNode *creat_bt2(){ BiTNode *t;printf("\n data="); scanf("%d",&e);if(e==0) t=NULL; /* 对于0值,不分配新结点 */else { t=(BiTNode *)malloc(sizeof(BiTNode));t->data=e;t->lch=creat_bt2(); /* 左孩子获得新指针值 */t->rch=creat_bt2(); /* 右孩子获得新指针值 */}return(t);} /* creat_bt2 *//* 中序递归遍历二叉树 */void inorder(BiTNode *p){ if (p) { inorder(p->lch);printf("%3d",p->data);inorder(p->rch);}} /* inorder *//* 利用中序递归遍历二叉树的方法,计算树中结点个数 *//* 读者可以试着运用先序或后序递归遍历二叉树方法重新编写这一段函数 */void inorder(BiTNode *p){ if (p) { inorder(p->lch);{ printf("%3d",p->data);n++;if(p->lch==NULL && p->lch==NULL) n0++;if((p->lch==NULL && p->lch!=NULL)||(p->lch!=NULL && p->lch==NULL) n1++;if(p->lch!=NULL && p->lch!=NULL) n2++;} /* 把访问的功能扩大了 */inorder(p->rch);}} /* inorder */。三、实习题建立一棵二叉树,要求用先序非递归方法遍历二叉树。建立一棵二叉树,要求用“按层遍历”的方法遍历二叉树”的函数。给定一组值,建立一棵二叉树,求二叉数的树深。哈夫曼树问题。利用哈夫曼编码进行通讯可以大大提高信道利用率,缩短信息传输时间,降低传输成本,但是,这要求在发送端通过一个编码系统对待传数据进行预先编码;在接受端将传来的数据进行解码(复原)对于双工信道(即可以双向传输的信道),每端都要有一个完整的编/译码系统。试为这样的信息收发站写一个哈夫曼的编译码系统。[基本要求]:A:从终端读入字符集大小为n,及n个字符和n个权值,建立哈夫曼树,进行编码并且输出。(选做)并将它存于文件hfmtree中。B:利用已建好的哈夫曼编码文件hfmtree,对键盘输入的正文进行译码。输出字符正文,再输出该文的二进制码。[测试数据] 用下表中给出的字符集和频度的实际统计数据建立哈夫曼树:字符 A B C D E F G H I J K L M N频度 64 13 22 32 103 21 15 47 57 1 5 32 20 57字符 O P Q R S T U V W X Y Z 空格频度 63 15 1 48 51 80 23 8 18 1 16 1 168并实现以下报文的译码和输出:“THIS PROGRAM IS MY FAVORITE”。实习七 图一、实习目的熟悉图的两种常用的存储结构,以及在这两种存储结构上的两种遍历图的方法,即深度优先遍历和广度优先遍历。进一步掌握递归算法的设计方法。关于各种典型著名的复杂算法,在上机实习方面不做基本要求。更适合于安排大型课程设计。二、实例图的邻接矩阵存储(数组表示)、简单输出。本题的目的是给出一个无向图数组表示的简单启示,在此基础上稍加改动可以实现网(边上带权值的图)的邻接矩阵表示。# include # include # define MAX 20typedef int VexType;typedef VexType Mgraph[MAX][MAX]; /* Mgraph是二维数组类型标识符 *//* 函数原形声明 */void creat_mg(Mgraph G);void out_mg(Mgraph G);Mgraph G1; /* G1是邻接矩阵的二维数组名 */int n,e,v0;/* 主函数 */main(){ creat_mg(G1); out_mg(G1);}/* main *//* 建立邻接矩阵 */void creat_mg(Mgraph G){ int i,j,k;printf(“\n n,e= ”); scanf(“%d%d”, &n,&e); /* 输入顶点数n,边数e */for(i=1; i<=n;i++)for(j=1;j<=n;j++) G[i][j]=0;/* 如果是网,G[i][j]=0该为G[i][j]=32767(无穷)*/for(k=1;k<=e;k++) /* 组织边数的循环 */{ printf(“\n vi,vj= ”);scanf(“%d%d”, &i,&j); /* 输入一条边的两个顶点编号i,j */G[i][j]=1; G[j][i]=1; /* 无向图的邻接矩阵是对称矩阵 *//* 如果是网,还要输入边的权值w,再让G[i][j]=w */}} /* creat_mg *//* 邻接矩阵简单输出,为了检查输入是否正确 */void out_mg(Mgraph G){ int i,j,k; char ch;for(i=1; i<=n;i++) /* 矩阵原样输出 */{ printf(“\n “);for(j=1;j<=n;j++) printf(“%5d”,G[i][j]);}/* 输出所存在的边 */for(i=1; i<=n;i++)for(j=1;j<=n;j++)if(G[i][j]==1)printf(“\n 存在边< %d,%d >”,i,j);printf("\n\n 打回车键,继续。“); ch=getch();} /* out_mg */图的邻接链表存储及递归深度优先遍历。# include # include # define MAX 20typedef int VexType;typedef struct Vnode{ VexType data;struct Vnode *next;}Vnode; /* Vnode是顶点的结点结构 */typedef Vnode Lgraph[MAX]; /* Lgraph是一维数组类型标识符 *//* 函数原形声明 */void creat_L(Lgraph G);void out_L(Lgraph G);void dfsL(Lgraph G,int v);Lgraph Ga; /* Ga是邻接链表的表头数组名 */int n,e, vis[MAX];/* 主函数 */main(){ int v1,i; char ch;for(i=0;icreat_L(Ga); /* 建立图邻接链表Ga */out_L(Ga); /* 输出邻接链表Ga */printf("\n "); scanf("%d",&v1);dfsL(Ga,v1); /* 从顶点v1开始,对图Ga进行深度优先遍历 */printf("\n\n 打回车键,继续。“); ch=getch();}/* main *//* 建立邻接链表 */void creat_L(Lgraph G){ Vnode *p,*q; int i,j,k;printf("输入 n,e= "); scanf("%d,%d",&n,&e);for(i=1; i<=n; i++) { G[i].data=i; G[i].next=NULL;}for(k=1;k<=e; k++){ printf("输入 vi,vj= ");scanf("%d,%d",&i,&j);p=(Vnode *)malloc(sizeof(Vnode));p->data=i;p->next=G[j].next; G[j].next=p; /* p结点链接到第j条链 */q=(Vnode *)malloc(sizeof(Vnode));q->data=j;q->next=G[i].next; G[i].next=q; /* q结点链接到第i条链 */}}/* creat_L *//* 邻接链表的简单输出,为了检查输入是否正确 */void out_L(Lgraph G){ int i; Vnode *p; char ch;for (i=1; i<=n; i++){ printf("\n i=%d",i);p=G[i].next;while(p!=NULL) { printf("%5d",p->data); p=p->next;}}printf("\n\n 打回车键,继续。“); ch=getch();} /* out_L *//* 深度优先遍历图 */void dfsL(Lgraph G,int v){ Vnode *p;printf("%3d",G[v].data); vis[v]=1; /* 顶点v被访问,标志置1 */p=G[v].next;while(p){ v=p->data;if(vis[v]==0)dfs(G,v); /* 顶点v未被访问时继续遍历 */p=p->next;}}/* dfs */注:图的广度优先遍用非递归方法容易理解,非递归方法需要辅助队列Q以及出队、入队函数。由于篇幅所限这里不在给出完整的源程序,仅给出粗略的算法(但比书上算法描述的更加具体详细)供参考。/* 广度优先遍历图 */void bfsL(Lgraph g,int v_){ char ch;printf("\n %d",g[v].data); vis[v]=1; enque(Q,v);while(Q.front!=Q.rear){ x=deque(Q);p=g[x].next;while(p){ v=p->data;if(vis[v]==0) { printf("\n %d",g[v].data);vis[v]=1; enque(Q,v);}p=p->next;}}printf("\n\n 打回车键,继续。“); ch=getch();} /* bfsL */三、实习题阅读理解上面第一个关于图的邻接矩阵的程序,做下列题目。根据教科书P157页的G2图(无向图),输入数据运行程序。再适当修改上述程序,使它适用于G1图(有向图),输入数据运行程序。提示:无向图的邻接矩阵是对称的,而有向图的邻接矩阵是非对称的。继续修改程序使之可以表示存储以下网(边上带权值的图)。提示:城市名暂时用代号(1,2,……)表示,在程序中以数组的下标表示城市名。它的邻接矩阵如下:2. 调试运行上面第二个程序,即图的邻接链表存储的程序。解决下列问题。根据教科书P157页的G2图(无向图),输入数据运行程序。再适当修改程序使它适用于G1图(有向图),输入数据运行程序。提示:有向图的邻接链表分为正邻接链表和逆邻接链表。3. 设计一个程序,建立图的邻接矩阵,并且进行图的深度优先遍历。结合第2题的图运行调试程序。图的一章中由各种典型、著名的复杂算法,在上机练习方面不做基本要求。更适合于安排大型课程设计。学生只要彻底搞清基本概念、基本存储结构,经过努力是可以完成的。实习八 查找一、实习目的掌握几种典型的查找方法(折半查找、二叉排序树的查找、哈希查找),并对各种算法的特点、使用范围和效率有进一步的了解。二、实例二叉排序树的建立、查找。设有一组数据(33,88,22,55,90,11,66,99),边输入边插入建立二叉排序树。查找77是否存在?99是否存在?[程序清单]# include # include typedef int Etype;typedef struct BiTNode /* 树结点结构 */{ Etype data;struct BiTNode *lch,*rch;}BiTNode;/* 函数原形声明 */BiTNode *creat_bt();void inorder(BiTNode *p);BiTNode *search(BiTNode *root,Etype e);void searchx(BiTNode *p,Etype e);BiTNode *t; int n,n0,n1,n2,;/* 主函数 */main(){ char ch; int k;t=creat_bt( ); /* 调用建立二叉排序树算法 */inorder(t); /* 调用中序遍历算法 */printf("\n 输入data:"); scanf(”%d”,&x);searchx(t,x);printf(“\n 打回车键,返回。“); ch=getch();} /* main */BiTNode *create_bt(){ int k; BiTNode *t=NULL,*s;int i,e;printf(“\n key= ”);scanf(“%d”,&k);while(k!=0){ s=(BiTNode *)malloc(sizeof(BiTNode));s->data=k; s->lch=NULL; s->rch=NULL;f=search(t,k); /* 调用查找算法,f是待找结点的父结点指针 */if(f=NULL)t=s;else { if(k<=f->data) f->lch=s; /* s做父结点f 的左孩子 */else f->rch=s; /* s做父结点f 的右孩子 */}printf(“\n Key= ”);scanf(“%d”,&k);}return(t);}/* create_bt *//* 查找算法,返回的是待找结点(e)的父结点指针,为插入提供条件 */BiTNode *search(BiTNode *root,Etype e){ BiTNode *p,*q;p=NULL; q=root;while(q){ p=q;if(e<=q->data)q=q->lch;else q=q->rch;}return(p); /* 返回值是待找结点(e)的父结点指针 */}/* search *//* 仅查找数据e 是否存在 */void searchx(BiTNode *root,int e){ BiTNode *p,*q; int tag=0;p=NULL; q=root;while(q && tag==0){ p=q;if(q->data==e)tag=1; /* 找到e之后,结束循环 */else if(k<=q->data)q=q->lch;else q=q->rch;}if(tag==1)printf(“\n yes!”);else printf(“\n no!”);}/* searchx *//* 中根遍历二叉排序树,结果应该是有序的,为了验证二叉排序树的正确性 */void display(BiTNode *t){ if( t!=NULL) { display(tlc);printf(“name:%s\t tel:%s\n”,tname,ttel);display(trc);}} /* display */三、实习题1.编写折半查找的算法的递归调用程序。2.设有一组关键字(19,14,23,1,68,20,84,27,56,11,10,79),建立一个哈希查找表。哈希函数采用: H(key)= key % P(其中P=13),若发生冲突后,用链地址法解决冲突。哈希函数采用: H(key)= key % P(其中P=13),若发生冲突后, 用线性探测再散列方法解决冲突。实习九 排序一、实习目的熟悉几种典型的排序方法,并对各种算法的特点、使用范围和效率有进一步的了解。二、实例排序是计算机科学中非常基本且使用频繁的运算,在计算机系统软件和应用软件中都有广泛的应用。下面给出的是冒泡排序、快速排序的实现与比较。/* 源程序的头文件 sort.h */void getrandat(Data ary[],int count) /* 产生一批随机数,准备排序 */{ long int a=100001;int i;for(i=0;iary[i].key=(int)a;}} /* getrandat */void prdata(Data ary[],int count) /* 输出数据的函数 */{ int i; char ch;printf(“\n”);for(i=0;iprintf(“\n”);printf(“\n\n 打回车键,结束显示。“); ch=getch();} /* prdata */[两种排序方法的比较源程序]/* sortcmp.c */#include #include #include #define MAX 1000 /* 数组最大界限 */typedef int ElemType; /* 关键字的类型 */typedef struct{ ElemType key;int shu; /* 其它属性域 */}Data; /* 一个纪录的结构体类型 */Data ar[MAX],br[MAX];Typedef struct{ int lo,hi;}Selem; /* 栈的元素结构体类型 */typedef struct{ Selem elem[MAX]; /* 一维数组子域 */int top; /* 栈顶指针子域 */}SqStack; /* 栈的顺序结构体类型 */Stack s1;/* 函数声明 */void bubble(Data ary[],int n)void qksort(Data ary[],int n)void hoare(Data ary[],int l,int h)void init_s(SqStack *s);void push(SqStack *s,Selem e) /* 进栈一个元素 */Selem pop(SqStack *s)int empty(SqStack s)/* 主函数 */main(){ int k,n,j; j; char ch;do { printf("\n\n\n");printf("\n\n 1. 产生一批随机数准备排序 ");printf("\n\n 2. 一般情况的起泡排序");printf("\n\n 3. 有序情况的起泡排序");printf("\n\n 4. 一般情况的快速排序");printf("\n\n 5. 有序情况的快速排序");printf("\n\n 6. 结束程序运行");printf("\n======================================");printf("\n 请输入您的选择 (1,2,3,4,5,6)");scanf("%d",&k);switch(k){ case 1:{ printf(“the number of datas:”); /* 输入数据个数 */scanf(“%d”,&n);getrandat(ar,n); /*产生n个随机数 */for(j=0;jprdata(ar,n);} break;case 2:{ for(j=0;jbubble(ar,n); /* 对n个数据起泡排序 */prdata(ar,n); /* 排序后输出 */} break;case 3: { bubble( ar,n); /* 有序情况的起泡排序 */prdata(ar,n);} break;case 4:{ for(j=0;jqksort(ar,n); /* 对n个数据快速排序 */prdata(ar,n); /* 排序后输出 */} break;case 5:{ qksort(ar,n); /* 有序情况的快速排序 */prdata(ar,n);} break;case 6: exit(0);} /* switch */printf("\n ----------------");}while(k>=1 && k<6);printf("\n 再见!")printf(“\n 打回车键,返回。“); ch=getch();} /* main *//* 起泡排序 */void bubble(Data ary[],int n){ int i,j,tag; Dtata x;i=0;do{ tag=0;for(j=0;jif (ary[j].key>ary[j+1].key){ x=ary[j]; ary[j]=ary[j+1];ary[j+1]=x; tag=1;}i++;}while( i} /* bubble *//* 快速排序非递归算法 */void qksort(Data ary[],int n){ int low,high,i,tag; Selem x,y;init_s( &s1); /* 初始化一个空栈 */low=0; high=n-1; tag=1;do{ while(low{ i=hoare(ary,low,high); /* 调用一趟快速排序 */x.lo= i+1; x.hi=high;push(&s1,x);high=i-1;}if(empty()==0) tag=0;else { y=pop(s1);low==y.lo; high=y.hi;}}while(tag==1);} /* qksort *//* 一趟快速排序 */void hoare(int ary[],int l,int h){ int i,j; Data x;i=l; j=h; x=ary[l];do{ while((i=x) j--;if (iwhile((iif(i}while(iary[i]=x; /* 枢轴记录定位 */return(i); /* 返回左、右区分界处 */} /* hoar *//* 关于栈的操作 */void init_s(SqStack *s) /* 初始化一个空栈 */{ s->top=0;} /* init_s */;/* 进栈一个元素 */void push(SqStack *s,Selem e){ if(s->top==MAX-1)printf(“\n stack Overflow!\n”);else { s->top++;s->elem[s->top]=e;}}/* push *//* 出栈一个元素 */Selem pop(SqStack *s) init_s( &s1);{ Selem e;if(s->top==0){ printf(“\n satck Empty!\n”);e.lo=-1; e.hi=-1;}else { e=s->elem[s->top];s->top--;}return(e);} /* pop */本程序中,qksort用非递归实现快速排序,在这个函数里,调用了hoare函数,函数,hoare对ary[l…h]进行一趟快速排序,执行该函数后,将ary[]从下标i处分成左右两个分区,其中左分区中的数据均小于等于ary[i].key,而右分区中的数据均大于等于ary[i].key(l≤i≤h)。将右区尾指针(记录下标)保存入栈,对左区再调用hoare进行快速排序。另外,此处用到的栈与以前的栈结构稍有不同,(因为栈中同时要放进两个值),故操作也稍有不同。例:入栈时,要两个参数即右区首、尾指针组成的结构体变量入栈。快速排序的最坏情况亦即各元素已有序时,再进行快速排序,这种情况下,其实并不快,这种情况下的冒泡排序反而很快。可见算法的优劣并不是绝对的。快速排序适合于记录关键字无序的情况。排序因其应用广泛,所以人们在排序找方面的研究经久不衰。三、实习题编写程序实现简单选择排序、堆排序(或归并排序),进行比较分析。编写程序实现简单插入排序、希尔排序(或基数排序),进行比较分析,第三部分 书面作业练习题习 题 一 绪 论.1.1 单项选择题1. 数据结构是一门研究非数值计算的程序设计问题中计算机的①以及它们之间的②和运算等的学科。① A.操作对象 B.计算方法 C.逻辑存储 D.数据映象② A.结构 B.关系 C.运算 D.算法2. 数据结构被形式地定义为(K,R),其中K是①的有限集合,R是K上的②有限集合。① A.算法 B.数据元素 C.数据操作 D.逻辑结构② A.操作 B.映象 C.存储 D.关系3. 在数据结构中,从逻辑上可以把数据结构分成①。A.动态结构和静态结构 B.紧凑结构和非紧凑结构C.线性结构和非线性结构 D.内部结构和外部结构4. 线性表的顺序存储结构是一种①的存储结构,线性表的链式存储结构是一种②的存储结构。A.随机存取 B.顺序存取 C.索引存取 D.散列存取5. 算法分析的目的是①,算法分析的两个主要方面是②。A. 找出数据结构的合理性 B. 研究算法中的输入和输出的关系C. 分析算法的效率以求改进 D. 分析算法的易懂性和文档性A. 空间复杂性和时间复杂性 B. 正确性和简明性C. 可读性和文档性 D. 数据复杂性和程序复杂性6. 计算机算法指的是①,它必具备输入、输出和②等五个特性。① A. 计算方法 B. 排序方法C. 解决问题的有限运算序列 D. 调度方法② A. 可行性、可移植性和可扩充性 B. 可行性、确定性和有穷性C. 确定性、有穷性和稳定性 D. 易读性、稳定性和安全性7. 线性表的逻辑顺序与存储顺序总是一致的,这种说法①。A. 正确 B. 不正确8. 线性表若采用链式存储结构时,要求内存中可用存储单元的地址①。A. 必须是连续的 B. 部分地址必须是连续的C. 一定是不连续的 D. 连续或不连续都可以9. 在以下的叙述中,正确的是①。线性表的线性存储结构优于链表存储结构二维数组是其数据元素为线性表的线性表栈的操作方式是先进先出队列的操作方式和先进后出10. 每种数据结构都具备三个基本运算:插入、删除和查找,这种说法①。A. 正确 B. 不正确1.2 填空题(将正确的答案填在相应的空中)1. 数据逻辑结构包括①、②和③三种类型,树形结构和图形结构合称为④。2. 在线性结构中,第一个结点①前驱结点,其余每个结点有且只有②个前驱结点;最后一个结点③后续结点,其余每个结点有且只有④个后续结点。3. 在树形结构中,树根结点没有①结点,其余每个结点有且只有②个前驱结点,叶子结点没有③结点,其余每个结点的后续结点可以④。4. 在图形结构中,每个结点的前驱结点数和后续结点数可以①。5. 线性结构中元素之间存在①关系,树形结构中元素之间存在②关系,图形结构中元素之间存在③关系。6. 算法的五个重要特性是____ ____ ____ ____ ____。7. 下面程序段的时间复杂度是①。for (i=0;ifor (j=0;jA[i][j]=0;8. 下面程序段的时间复杂度是①。i=s=0;while (s{ i++; /*i=i+1*/s+=i; /*s=s+1*/}9. 下面程序段的时间复杂度是①。s=0;for (i=0;ifor (j=0;js+=B[i][j];sum=s;10. 下面程序段的时间复杂度是①。i=1;while (i<=n)i=i*3;1.3 算法设计题:1. 试写一算法,自大到小依次输出顺序读入的三个数X,Y和Z的值.习题答案1.1 1. AB 2. BD 3. C 4. AB 5. CA 6. CB 7. B 8. D9. B 10. B1.2 1. 线性结构、树形结构、图形结构、非线性结构2. 没有、1、没有、13. 前驱、1、后续、任意多个4. 任意多个5. 一对一、一对多、多对多6. 有穷性、确定性、可行性、输入、输出7. O(m*n)8. O (n)9. O (n2)10. log3n习 题 二 顺序表示(线性表、栈和队列)2.1 单项选择题1. 一个向量第一个元素的存储地址是100,每个元素的长度为2,则第5个元素的地址是____。A. 110 B. 108 C. 100 D. 1202. 一个栈的入栈序列a,b,c,d,e,则栈的不可能的输出序列是____。A. edcba B. decba C. dceab D. abcde3. 若已知一个栈的入栈序列是1,2,3,…,n,其输出序列为p1,p2,p3,…,pn,若p1=n,则pi为____。A. i B. n=i C. n-i+1 D. 不确定4. 栈结构通常采用的两种存储结构是____。顺序存储结构和链式存储结构散列方式和索引方式链表存储结构和数组线性存储结构和非线性存储结构5. 判定一个栈ST(最多元素为m0)为空的条件是____。A. ST—> top !=0 B. ST—> top= =0C. ST—> top !=m0 D. ST—> top= =m06. 判定一个栈ST(最多元素为m0)为栈满的条件是____。A. ST—> top!=0 B. ST—> top= =0C. ST—> top!=m0 D. ST—> top= =m07. 栈的特点是____,队列的特点是____。A. 先进先出 B. 先进后出8. 一个队列的入列序列是1,2,3,4,则队列的输出序列是____ 。A. 4,3,2,1 B. 1,2,3,4C. 1,4,3,2 D. 3,2,4,19. 判定一个队列QU(最多元素为m0)为空的条件是____。QU—>rear—QU—>front= =m0QU—>rear—QU—>front-1= =m0QU—>front= =QU—>rearQU—>front= =QU—>rear+110. 判定一个队列QU(最多元素为m0, m0+1= =Maxsize)为满队列的条件是____。((QU—>rear-QU—>front)+ Maxsize)% Maxsize = =m0QU—>rear—QU—>front-1= =m0QU—>front= =QU—>rearQU—>front= =QU—>rear+111. 判定一个循环队列QU(最多元素为m0)为空的条件是____。QU—>front= =QU—>rearQU—>front!=QU—>rearQU—>front= =(QU—>rear+1)%m0QU—>front!=(QU—>rear+1)%m012. 判定一个循环队列QU(最多元素为m0)为满队列的条件是____。QU—>front= =QU—>rearQU—>front!=QU—>rearQU—>front= =(QU—>rear+1)%m0QU—>front!=(QU—>rear+1)%m013. 循环队列用数组A[0,m-1]存放其元素值,已知其头尾指针分别是front和rear,则当前队列中的元素个数是____。A. (rear-front+m)%m B. rear-front+1C. rear-front-1 D. rear-front14. 栈和队列的共同点是____。A. 都是先进后出 B. 都是先进先出C. 只允许在端点处插入和删除元素 D. 没有共同点2.2 填空题(将正确的答案填在相应的空中)向量、栈和队列都是____结构,可以在向量的____位置插入和删除元素;对于栈只能在____插入和删除元素;对于队列只能在____插入元素和____删除元素。向一个长度为n的向量的第i个元素(1≤i≤n+1)之前插入一个元素时,需向后移动____个元素。向一个长度为n的向量中删除第i个元素(共149张PPT)线性表是一种最简单的线性结构线性结构的基本特征:1.集合中必存在唯一的一个“第一元素”;2.集合中必存在唯一的一个 “最后元素”3.除最后元素在外,每个元素均有 唯一的后继;4.除第一元素之外,每个元素均有 唯一的前驱。线性结构是一个数据元素的有序(次序)集2.1 线性表的类型定义2.3 线性表类型的实现 链式映象2.4 一元多项式的表示2.2 线性表类型的实现 顺序映象抽象数据类型线性表的定义如下:ADT List {数据对象:D={ ai | ai ∈ElemSet, i=1,2,...,n, n≥0 }{称 n 为线性表的表长;称 n=0 时的线性表为空表。}数据关系:R1={ |ai-1 ,ai∈D, i=2,...,n }{设线性表为 (a1,a2, . . . ,ai,. . . ,an),称 i 为 ai 在线性表中的位序。}基本操作:结构初始化操作结构销毁操作引用型操作加工型操作} ADT ListInitList( &L )操作结果:构造一个空的线性表L。初始化操作结构销毁操作DestroyList( &L )初始条件:操作结果:线性表 L 已存在。销毁线性表 L。ListEmpty( L )ListLength( L )PriorElem( L, cur_e, &pre_e )NextElem( L, cur_e, &next_e )GetElem( L, i, &e )LocateElem( L, e, compare( ) )ListTraverse(L, visit( ))引用型操作:ListEmpty( L )初始条件:操作结果:线性表L已存在。若L为空表,则返回TRUE,否则FALSE。(线性表判空)ListLength( L )初始条件:操作结果:线性表L已存在。返回L中元素个数。(求线性表的长度)PriorElem( L, cur_e, &pre_e )初始条件:操作结果:线性表L已存在。若cur_e是L的元素,但不是第一个,则用pre_e 返回它的前驱,否则操作失败,pre_e无定义。(求数据元素的前驱)NextElem( L, cur_e, &next_e )初始条件:操作结果:线性表L已存在。若cur_e是L的元素,但不是最后一个,则用next_e返回它的后继,否则操作失败,next_e无定义。(求数据元素的后继)GetElem( L, i, &e )初始条件:操作结果:线性表L已存在,且 1≤i≤LengthList(L)用 e 返回L中第 i 个元素的值。(求线性表中某个数据元素)LocateElem( L, e, compare( ) )初始条件:操作结果:线性表L已存在,e为给定值,compare( )是元素判定函数。返回L中第1个与e满足关系compare( )的元素的位序。若这样的元素不存在,则返回值为0。(定位函数)ListTraverse(L, visit( ))初始条件:操作结果:线性表L已存在。Visit() 为某个访问函数。依次对L的每个元素调用函数visit( )。一旦visit( )失败,则操作失败。(遍历线性表)ClearList( &L )初始条件:操作结果:线性表L已存在。将L重置为空表。(线性表置空)加工型操作ClearList( &L )PutElem( &L, i, &e )ListInsert( &L, i, e )ListDelete(&L, i, &e)PutElem( &L, i, &e )初始条件:操作结果:线性表L已存在,且 1≤i≤LengthList(L)L中第i个元素赋值同e的值。(改变数据元素的值)ListInsert( &L, i, e )初始条件:操作结果:线性表L已存在,且 1≤i≤LengthList(L)+1在L的第i个元素之前插入新的元素e,L的长度增1。(插入数据元素)ListDelete(&L, i, &e)初始条件:操作结果:线性表L已存在且非空,1≤i≤LengthList(L)删除L的第i个元素,并用e返回其值,L的长度减1。(删除数据元素)利用上述定义的线性表可以实现其它更复杂的操作例 2-2例 2-3例 2-1假设:有两个集合 A 和 B 分别用两个线性表 LA 和 LB 表示,即:线性表中的数据元素即为集合中的成员。现要求一个新的集合A=A∪B。例 2-1要求对线性表作如下操作:扩大线性表 LA,将存在于线性表LB 中而不存在于线性表 LA 中的数据元素插入到线性表 LA 中去。上述问题可演绎为:1.从线性表LB中依次察看每个数据元素;2.依值在线性表LA中进行查访;3.若不存在,则插入之。GetElem(LB, i)→eLocateElem(LA, e, equal( ))ListInsert(LA, n+1, e)操作步骤:GetElem(Lb, i, e); // 取Lb中第i个数据元素赋给eif (!LocateElem(La, e, equal( )) )ListInsert(La, ++La_len, e);// La中不存在和 e 相同的数据元素,则插入之void union(List &La, List Lb) {La_len = ListLength(La); // 求线性表的长度Lb_len = ListLength(Lb);for (i = 1; i <= Lb_len; i++) {}} // union已知一个非纯集合 B,试构造一个纯集合 A,使 A中只包含 B 中所有值各不相 同的数据元素。仍选用线性表表示集合例 2-2集合 B集合 A从集合 B 取出物件放入集合 A要求集合A中同样物件不能有两件以上因此,算法的策略应该和例2-1相同void union(List &La, List Lb) {La_len=ListLength(La); Lb_len=ListLength(Lb);} // unionGetElem(Lb, i, e); // 取Lb中第 i 个数据元素赋给 eif (!LocateElem(La, e, equal( )) )ListInsert(La, ++La_len, e);// La中不存在和 e 相同的数据元素,则插入之for (i = 1; i <= Lb_len; i++) {}InitList(La); // 构造(空的)线性表LA若线性表中的数据元素相互之间可以比较,并且数据元素在线性表中依值非递减或非递增有序排列,即ai≥ai-1 或 ai≤ai-1(i = 2,3,…, n),则称该线性表为有序表(Ordered List)。试改变结构,以有序表表示集合。例如:(2,3,3,5,6,6,6,8,12)对集合 B 而言,值相同的数据元素必定相邻对集合 A 而言,数据元素依值从小至大的顺序插入因此,数据结构改变了,解决问题的策略也相应要改变。void purge(List &La, List Lb) {InitList(LA); La_len = ListLength(La);Lb_len =ListLength(Lb); // 求线性表的长度for (i = 1; i <= Lb_len; i++) {}} // purgeGetElem(Lb, i, e); // 取Lb中第i个数据元素赋给 eif (ListEmpty(La) || !equal (en, e)) {ListInsert(La, ++La_len, e);en = e;} // La中不存在和 e 相同的数据元素,则插入之则归并两个“其数据元素按值非递减有序排列”的有序表 LA 和 LB,求得有序表 LC 也具有同样特性。设 La = (a1, …, ai, …, an), Lb = (b1, …, bj, …, bm)Lc = (c1, …, ck, …, cm+n)且已由(a1, …, ai-1)和(b1, …,bj-1)归并得 (c1, …, ck-1)例 2-3k = 1, 2, …, m+n1.初始化 LC 为空表;基本操作:2.分别从 LA和LB中取得当前元素 ai 和 bj;3.若 ai≤bj,则将 ai 插入到 LC 中,否则将bj 插入到 LC 中;4.重复 2 和 3 两步,直至 LA 或 LB 中元素被取完为止;5.将 LA 表或 LB 表中剩余元素复制插入到LC 表中。// La 和 Lb 均非空,i = j = 1, k = 0GetElem(La, i, ai);GetElem(Lb, j, bj);if (ai <= bj) { // 将 ai 插入到 Lc 中ListInsert(Lc, ++k, ai); ++i; }else { // 将 bj 插入到 Lc 中ListInsert(Lc, ++k, bj); ++j; }void MergeList(List La, List Lb, List &Lc) {// 本算法将非递减的有序表 La 和 Lb 归并为 Lc} // merge_listwhile ((i <= La_len) && (j <= Lb_len)){ // La 和 Lb 均不空 }while (i<=La_len) // 若 La 不空while (j<=Lb_len) // 若 Lb 不空InitList(Lc); // 构造空的线性表 Lci = j = 1; k = 0;La_len = ListLength(La);Lb_len = ListLength(Lb);while (i <= La_len) { // 当La不空时GetElem(La, i++, ai);ListInsert(Lc, ++k, ai);} // 插入 La 表中剩余元素while (j <= Lb_len) { // 当Lb不空时GetElem(Lb, j++, bj);ListInsert(Lc, ++k, bj);} // 插入 Lb 表中剩余元素最简单的一种顺序映象方法是:令 y 的存储位置和 x 的存储位置相邻。顺序映象—— 以 x 的存储位置和 y 的存储位置之间某种关系表示逻辑关系用一组地址连续的存储单元依次存放线性表中的数据元素a1 a2 … ai-1 ai … an线性表的起始地址,称作线性表的基地址以“存储位置相邻”表示有序对即:LOC(ai) = LOC(ai-1) + C一个数据元素所占存储量↑所有数据元素的存储位置均取决于第一个数据元素的存储位置LOC(ai) = LOC(a1) + (i-1)×C↑基地址顺序映像的 C 语言描述typedef struct {} SqList; // 俗称 顺序表#define LIST_INIT_SIZE 80// 线性表存储空间的初始分配量#define LISTINCREMENT 10// 线性表存储空间的分配增量ElemType *elem; // 存储空间基址int length; // 当前长度int listsize; // 当前分配的存储容量// (以sizeof(ElemType)为单位)线性表的基本操作在顺序表中的实现InitList(&L) // 结构初始化LocateElem(L, e, compare()) // 查找ListInsert(&L, i, e) // 插入元素ListDelete(&L, i) // 删除元素Status InitList_Sq( SqList& L, int maxsize ) {// 构造一个最大容量为 list_init_size的顺序表} // InitList_Sq算法时间复杂度:O(1)L.elem = (ElemType﹡)malloc(list_init_size*sizeof(elemtype));// 为顺序表分配大小为 maxsize 的数组空间if (!L.elem) exit(OVERFLOW);L.length = 0;L.listsize = list_init_size;return OK;int LocateElem_Sq(SqList L, ElemType e,Status (*compare)(ElemType, ElemType)) {// 在顺序表中查询第一个满足判定条件的数据元素,// 若存在,则返回它的位序,否则返回 0} // LocateElem_SqO( ListLength(L) )算法的时间复杂度为:i = 1; // i 的初值为第 1 元素的位序p = L.elem; // p 的初值为第 1 元素的存储位置while (i <= L.length &&!(*compare)(*p++, e)) ++i;if (i <= L.length) return i;else return 0;(*compare)(*p++, e)例如:顺序表23 75 41 38 54 62 17L.elemL.length = 7L.listsizee =38pppppi12341850p可见,基本操作是,将顺序表中的元素逐个和给定值 e相比较。线性表操作ListInsert(&L, i, e)的实现:首先分析:插入元素时,线性表的逻辑结构发生什么变化?(a1, …, ai-1, ai, …, an) 改变为a1 a2 … ai-1 ai … ana1 a2 … ai-1…aiean, 表的长度增加(a1, …, ai-1, e, ai, …, an)Status ListInsert_Sq(SqList &L, int i, ElemType e) {// 在顺序表L的第 i 个元素之前插入新的元素e,// i 的合法范围为 1≤i≤L.length+1} // ListInsert_Sq算法时间复杂度为:O( ListLength(L) )q = &(L.elem[i-1]); // q 指示插入位置for (p = &(L.elem[L.length-1]); p >= q; --p)*(p+1) = *p; // 插入位置及之后的元素右移*q = e; // 插入e++L.length; // 表长增1return OK;……元素右移考虑移动元素的平均情况:假设在第 i 个元素之前插入的概率为 ,则在长度为n 的线性表中插入一个元素所需移动元素次数的期望值为:若假定在线性表中任何一个位置上进行插入的概率都是相等的,则移动元素的期望值为:if (L.length >= L.listsize) {// 当前存储空间已满,增加分配newbase = (ElemType *)realloc(L.elem,(L.listsize+LISTINCREMENT)*sizeof (ElemType));if (!newbase) exit(OVERFLOW);// 存储分配失败L.elem = newbase; // 新基址L.listsize += LISTINCREMENT; // 增加存储容量}if (i < 1 || i > L.length+1) return ERROR;// 插入位置不合法21 18 30 75 42 56 8721 18 30 75例如:ListInsert_Sq(L, 5, 66)L.length-10pppq87564266q = &(L.elem[i-1]); // q 指示插入位置for (p = &(L.elem[L.length-1]); p >= q; --p)*(p+1) = *p;p线性表操作ListDelete(&L, i, &e)的实现:首先分析:删除元素时,线性表的逻辑结构发生什么变化?(a1, …, ai-1, ai, ai+1, …, an) 改变为ai+1…an, 表的长度减少a1 a2 … ai-1 ai ai+1 … ana1 a2 … ai-1(a1, …, ai-1, ai+1, …, an)Status ListDelete_Sq(SqList &L, int i, ElemType &e) {} // ListDelete_Sqfor (++p; p <= q; ++p) *(p-1) = *p;// 被删除元素之后的元素左移--L.length; // 表长减1return OK;算法时间复杂度为:O( ListLength(L))p = &(L.elem[i-1]); // p 为被删除元素的位置e = *p; // 被删除元素的值赋给 eq = L.elem+L.length-1; // 表尾元素的位置if ((i < 1) || (i > L.length)) return ERROR;// 删除位置不合法元素左移考虑移动元素的平均情况:假设删除第 i 个元素的概率为 ,则在长度为n 的线性表中删除一个元素所需移动元素次数的期望值为:若假定在线性表中任何一个位置上进行删除的概率都是相等的,则移动元素的期望值为:21 18 30 75 42 56 8721 18 30 75L.length-10pppq8756p = &(L.elem[i-1]);q = L.elem+L.length-1;for (++p; p <= q; ++p) *(p-1) = *p;例如:ListDelete_Sq(L, 5, e)p一、单链表二、结点和单链表的 C 语言描述三、线性表的操作在单链表中的实现四、一个带头结点的单链表类型五、其它形式的链表六、有序表类型用一组地址任意的存储单元存放线性表中的数据元素。一、单链表以元素(数据元素的映象)+ 指针(指示后继元素存储位置)= 结点(表示数据元素 或 数据元素的映象)以“结点的序列”表示线性表 称作链表以线性表中第一个数据元素 的存储地址作为线性表的地址,称作线性表的头指针头结点a1 a2 … ... an ^头指针头指针有时为了操作方便,在第一个结点之前虚加一个“头结点”,以指向头结点的指针为链表的头指针空指针线性表为空表时,头结点的指针域为空 以线性表中第一个数据元素 的存储地址作为线性表的地址,称作线性表的头指针Typedef struct LNode {ElemType data; // 数据域struct Lnode *next; // 指针域} LNode, *LinkList;二、结点和单链表的 C 语言描述LinkList L; // L 为单链表的头指针三、单链表操作的实现GetElem(L, i, e) // 取第i个数据元素ListInsert(&L, i, e) // 插入数据元素ListInsert(&L, i, e) // 删除数据元素ClearList(&L) // 重置线性表为空表CreateList(&L, n)// 生成含 n 个数据元素的链表L线性表的操作GetElem(L, i, &e)在单链表中的实现:211830754256∧pppj123因此,查找第 i 个数据元素的基本操作为:移动指针,比较 j 和 i单链表是一种顺序存取的结构,为找第 i 个数据元素,必须先找到第 i-1 个数据元素。令指针 p 始终指向线性表中第 j 个数据元素Status GetElem_L(LinkList L, int i, ElemType &e) {// L是带头结点的链表的头指针,以 e 返回第 i 个元素} // GetElem_L算法时间复杂度为:O(ListLength(L))p = L->next; j = 1; // p指向第一个结点,j为计数器while (p && jnext; ++j; }// 顺指针向后查找,直到 p 指向第 i 个元素// 或 p 为空if ( !p || j>i )return ERROR; // 第 i 个元素不存在e = p->data; // 取得第 i 个元素return OK;ai-1线性表的操作 ListInsert(&L, i, e)在单链表中的实现:有序对 改变为 和eaiai-1因此,在单链表中第 i 个结点之前进行插入的基本操作为:找到线性表中第i-1个结点,然后修改其指向后继的指针。可见,在链表中插入结点只需要修改指针。但同时,若要在第 i 个结点之前插入元素,修改的是第 i-1 个结点的指针。Status ListInsert_L(LinkList L, int i, ElemType e) {// L 为带头结点的单链表的头指针,本算法// 在链表中第i 个结点之前插入新的元素 e} // LinstInsert_L算法的时间复杂度为:O(ListLength(L))……p = L; j = 0;while (p && j < i-1){ p = p->next; ++j; } // 寻找第 i-1 个结点if (!p || j > i-1)return ERROR; // i 大于表长或者小于1s = new LNode;// 生成新结点s->data = e;s->next = p->next; p->next = s; // 插入return OK;eai-1aiai-1sp线性表的操作ListDelete (&L, i, &e)在链表中的实现:有序对 和 改变为 ai-1aiai+1ai-1在单链表中删除第 i 个结点的基本操作为:找到线性表中第i-1个结点,修改其指向后继的指针。ai-1aiai+1ai-1q = p->next; p->next = q->next;e = q->data; free(q);pqStatus ListDelete_L(LinkList L, int i, ElemType &e) {// 删除以 L 为头指针(带头结点)的单链表中第 i 个结点} // ListDelete_L算法的时间复杂度为:O(ListLength(L))p = L; j = 0;while (p->next && j < i-1) { p = p->next; ++j; }// 寻找第 i 个结点,并令 p 指向其前趋if (!(p->next) || j > i-1)return ERROR; // 删除位置不合理q = p->next; p->next = q->next; // 删除并释放结点e = q->data; free(q);return OK;操作 ClearList(&L) 在链表中的实现:void ClearList(&L) {// 将单链表重新置为一个空表while (L->next) {p=L->next; L->next=p->next;}} // ClearListfree(p);算法时间复杂度:O(ListLength(L))如何从线性表得到单链表?链表是一个动态的结构,它不需要予分配空间,因此生成链表的过程是一个结点“逐个插入” 的过程。例如:逆位序输入 n 个数据元素的值,建立带头结点的单链表。操作步骤:一、建立一个“空表”;二、输入数据元素an,建立结点并插入;三、输入数据元素an-1,建立结点并插入;ananan-1四、依次类推,直至输入a1为止。void CreateList_L(LinkList &L, int n) {// 逆序输入 n 个数据元素,建立带头结点的单链表} // CreateList_L算法的时间复杂度为:O(Listlength(L))L = (LinkList) malloc (sizeof (LNode));L->next = NULL; // 先建立一个带头结点的单链表for (i = n; i > 0; --i) {p = (LinkList) malloc (sizeof (LNode));scanf(&p->data); // 输入元素值p->next = L->next; L->next = p; // 插入}回顾 2.1 节中三个例子的算法,看一下当线性表分别以顺序存储结构和链表存储结构实现时,它们的时间复杂度为多少?void union(List &La, List Lb) {La_len = ListLength(La); Lb_len =ListLength(Lb);for (i = 1; i <= Lb_len; i++) {GetElem(Lb, i, e);if (!LocateElem(La, e, equal( ))ListInsert(La, ++La_len, e);}//for} // union控制结构:基本操作:for 循环GetElem, LocateElem 和 ListInsert当以顺序映像实现抽象数据类型线性表时为:O( ListLength(La)×ListLength(Lb) )当以链式映像实现抽象数据类型线性表时为:O( ListLength(La)×ListLength(Lb) )例2-1算法时间复杂度void purge(List &La, List Lb) {InitList(La);La_len = ListLength(La); Lb_len =ListLength(Lb);for (i = 1; i <= Lb_len; i++) {GetElem(Lb, i, e);if (ListEmpty(La) || !equal (en, e)){ ListInsert(La, ++La_len, e); en = e; }}//for} // purge控制结构:基本操作:for 循环GetElem 和 ListInsert当以顺序映像实现抽象数据类型线性表时为:O( ListLength(Lb) )当以链式映像实现抽象数据类型线性表时为:O( ListLength2(Lb) )例2-2算法时间复杂度void MergeList(List La, List Lb, List &Lc) {InitList(Lc); i = j = 1; k = 0;La_len = ListLength(La); Lb_len = ListLength(Lb);while ((i <= La_len) && (j <= Lb_len)) {GetElem(La, i, ai); GetElem(Lb, j, bj);if (ai <= bj) {ListInsert(Lc, ++k, ai); ++i; }else { ListInsert(Lc, ++k, bj); ++j; }} … …控制结构:基本操作:三个并列的while循环GetElem, ListInsert当以顺序映像实现抽象数据类型线性表时为:O( ListLength(La)+ListLength(Lb) )当以链式映像实现抽象数据类型线性表时为:O( ListLength 2(La)+ListLength 2(Lb) )例2-3算法时间复杂度用上述定义的单链表实现线性表的操作时,存在的问题:改进链表的设置:1.单链表的表长是一个隐含的值;1.增加“表长”、“表尾指针” 和 “当前位置的指针” 三个数据域;2.在单链表的最后一个元素之后插入元素时,需遍历整个链表;3.在链表中,元素的“位序”概念淡化,结点的“位置”概念加强。2.将基本操作中的“位序 i ”改变为“指针 p ”。四、一个带头结点的线性链表类型typedef struct LNode { // 结点类型ElemType data;struct LNode *next;} *Link, *Position;Status MakeNode( Link &p, ElemType e );// 分配由 p 指向的值为e的结点,并返回OK;// 若分配失败,则返回 ERRORvoid FreeNode( Link &p );// 释放 p 所指结点typedef struct { // 链表类型Link head, tail; // 分别指向头结点和// 最后一个结点的指针int len; // 指示链表长度Link current; // 指向当前被访问的结点//的指针,初始位置指向头结点} LinkList;链表的基本操作:{结构初始化和销毁结构}Status InitList( LinkList &L );// 构造一个空的线性链表 L,其头指针、// 尾指针和当前指针均指向头结点,// 表长为零。Status DestroyList( LinkList &L );// 销毁线性链表 L,L不再存在。O(1)O(n){引用型操作}Status ListEmpty ( LinkList L ); //判表空int ListLength( LinkList L ); // 求表长Status Prior( LinkList L );// 改变当前指针指向其前驱Status Next ( LinkList L );// 改变当前指针指向其后继ElemType GetCurElem ( LinkList L );// 返回当前指针所指数据元素O(1)O(1)O(n)O(1)O(1)Status LocatePos( LinkList L, int i );// 改变当前指针指向第i个结点Status LocateElem (LinkList L, ElemType e,Status (*compare)(ElemType, ElemType));// 若存在与e 满足函数compare( )判定关系的元// 素,则移动当前指针指向第1个满足条件的// 元素的前驱,并返回OK; 否则返回ERRORStatus ListTraverse(LinkList L, Status(*visit)() );// 依次对L的每个元素调用函数visit()O(n)O(n)O(n){加工型操作}Status ClearList ( LinkList &L );// 重置 L 为空表Status SetCurElem(LinkList &L, ElemType e );// 更新当前指针所指数据元素Status Append ( LinkList &L, Link s );// 在表尾结点之后链接一串结点Status InsAfter ( LinkList &L, Elemtype e );// 将元素 e 插入在当前指针之后Status DelAfter ( LinkList &L, ElemType& e );// 删除当前指针之后的结点O(1)O(n)O(s)O(1)O(1)Status InsAfter( LinkList& L, ElemType e ) {// 若当前指针在链表中,则将数据元素e插入在线性链// 表L中当前指针所指结点之后,并返回OK;// 否则返回ERROR。} // InsAfterif ( ! L.current ) return ERROR;if (! MakeNode( s, e) ) return ERROR;s->next = L.current->next;L.current->next = s;if (L.tail = L.current) L.tail = s;L.current = s; ++L.len; return OK;Status DelAfter( LinkList& L, ElemType& e ) {// 若当前指针及其后继在链表中,则删除线性链表L中当前// 指针所指结点之后的结点,并返回OK; 否则返回ERROR。} //DelAfterif ( !(L.current && L.current->next ) )return ERROR;q = L.current->next;L.current->next = q->next;if (L.tail = q) L.tail = L.current;e=q->data; FreeNode(q); L.len=len-1;return OK;例一Status ListInsert_L(LinkList L, int i, ElemType e) {// 在带头结点的单链线性表 L 的第 i 个元素之前插入元素 e} // ListInsert_L利用上述定义的线性链表如何完成 线性表的其它操作 ?if (!LocatePos (L, i-1)) return ERROR;// i 值不合法,第 i-1 个结点不存在if (InsAfter (L, e)) return OK; // 完成插入else return ERROR;Status MergeList_L(LinkList &Lc, LinkList &La,LinkList &Lb ,int (*compare)(ElemType,ElemType))) {// 归并有序表 La 和 Lb ,生成新的有序表 Lc,// 并在归并之后销毁La 和 Lb,// compare 为指定的元素大小判定函数……} // MergeList_L例二if ( !InitList(Lc)) return ERROR; // 存储空间分配失败while (!( a=MAXC && b=MAXC)) { // La 或 Lb 非空}… …LocatePos (La, 0); LocatePos (Lb, 0); // 当前指针指向头结点if ( DelAfter( La, e)) a = e; // 取得 La 表中第一个元素 aelse a = MAXC; // MAXC为常量最大值if ( DelFirst( Lb, e)) b = e; // 取得 Lb 表中第一个元素 belse b = MAXC; // a 和 b 为两表中当前比较元素DestroyList(La); DestroyList(Lb); // 销毁链表 La 和 Lbreturn OK;if ((*compare)(a, b) <=0) { // a≤bInsAfter(Lc, a);if ( DelAfter( La, e1) ) a = e1;else a = MAXC;}else { // a>bInsAfter(Lc, b);if ( DelAfter( Lb, e1) ) b = e1;else b = MAXC;}1. 双向链表五、其它形式的链表typedef struct DuLNode {ElemType data; // 数据域struct DuLNode *prior;// 指向前驱的指针域struct DuLNode *next;// 指向后继的指针域} DuLNode, *DuLinkList;最后一个结点的指针域的指针又指回第一个结点的链表a1 a2 … ... an2. 循环链表和单链表的差别仅在于,判别链表中最后一个结点的条件不再是“后继是否为空”,而是“后继是否为头结点”。双向循环链表空表非空表a1 a2 … ... an双向链表的操作特点:“查询” 和单链表相同“插入” 和“删除”时需要同时修改两个方向上的指针。ai-1aies->next = p->next; p->next = s;s->next->prior = s; s->prior = p;psai-1ai插入ai-1删除aiai+1p->next = p->next->next;p->next->prior = p;pai-1六、有序表类型ADT Ordered_List {数据对象: S = { xi|xi OrderedSet ,i=1,2,…,n, n≥0 }集合中任意两个元素之间均可以进行比较数据关系:R = { | xi-1, xi S,xi-1≤ xi, i=2,3,…,n }回顾例2-2的两个算法基本操作:… …… …LocateElem( L, e, &q,int(*compare)(ElemType,ElemType) )初始条件:有序表L已存在。操作结果:若有序表L中存在元素e,则 q指示L中第一个值为 e 的元素的位置,并返回函数值TRUE;否则 q 指示第一个大于 e 的元素的前驱的位置,并返回函数值FALSE。Compare是一个有序判定函数( 12, 23, 34, 45, 56, 67, 78, 89, 98, 45 )例如:若 e = 45,则 q 指向若 e = 88,则 q 指向表示值为 88 的元素应插入在该指针所指结点之后。void union(List &La, List Lb) {// Lb 为线性表InitList(La); // 构造(空的)线性表LALa_len=ListLength(La); Lb_len=ListLength(Lb);for (i = 1; i <= Lb_len; i++) {GetElem(Lb, i, e); // 取Lb中第 i 个数据元素赋给 eif (!LocateElem(La, e, equal( )) )ListInsert(La, ++La_len, e);// La中不存在和 e 相同的数据元素,则插入之}} // union算法时间复杂度:O(n2)void purge(List &La, List Lb) { // Lb为有序表InitList(LA); La_len = ListLength(La);Lb_len =ListLength(Lb); // 求线性表的长度for (i = 1; i <= Lb_len; i++) {}} // purgeGetElem(Lb, i, e); // 取Lb中第i个数据元素赋给 eif (ListEmpty(La) || !equal (en, e)) {ListInsert(La, ++La_len, e);en = e;} // La中不存在和 e 相同的数据元素,则插入之算法时间复杂度:O(n)在计算机中,可以用一个线性表来表示:P = (p0, p1, …,pn)一元多项式但是对于形如S(x) = 1 + 3x10000 – 2x20000的多项式,上述表示方法是否合适?一般情况下的一元稀疏多项式可写成Pn(x) = p1xe1 + p2xe2 + ┄ + pmxem其中:pi 是指数为ei 的项的非零系数,0≤ e1 < e2 < ┄ < em = n可以下列线性表表示:((p1, e1), (p2, e2), ┄, (pm,em) )P999(x) = 7x3 - 2x12 - 8x999例如:可用线性表( (7, 3), (-2, 12), (-8, 999) )表示ADT Polynomial {数据对象:数据关系:抽象数据类型一元多项式的定义如下:D={ ai | ai ∈TermSet, i=1,2,...,m, m≥0TermSet 中的每个元素包含一个表示系数的实数和表示指数的整数 }R1={ |ai-1 ,ai∈D, i=2,...,n且ai-1中的指数值<ai中的指数值 }CreatPolyn ( &P, m )DestroyPolyn ( &P )PrintPolyn ( &P )基本操作:操作结果:输入 m 项的系数和指数,建立一元多项式 P。初始条件:一元多项式 P 已存在。操作结果:销毁一元多项式 P。初始条件:一元多项式 P 已存在。操作结果:打印输出一元多项式 P。PolynLength( P )AddPolyn ( &Pa, &Pb )SubtractPolyn ( &Pa, &Pb )… …} ADT Polynomial初始条件:一元多项式 P 已存在。操作结果:返回一元多项式 P 中的项数。初始条件:一元多项式 Pa 和 Pb 已存在。操作结果:完成多项式相加运算,即:Pa = Pa+Pb,并销毁一元多项式 Pb。一元多项式的实现:typedef struct { // 项的表示float coef; // 系数int expn; // 指数} term, ElemType;typedef OrderedLinkList polynomial;// 用带表头结点的有序链表表示多项式结点的数据元素类型定义为:Status CreatPolyn ( polynomail &P, int m ) {// 输入m项的系数和指数,建立表示一元多项式的有序链表P} // CreatPolynInitList (P); e.coef = 0.0; e.expn = -1;SetCurElem (h, e); // 设置头结点的数据元素for ( i=1; i<=m; ++i ) { // 依次输入 m 个非零项}return OK;scanf (e.coef, e.expn);if (!LocateElem ( P, e, (*cmp)()) )if ( !InsAfter ( P, e ) ) return ERROR;注意: 1.输入次序不限;2.指数相同的项只能输入一次Status AddPolyn ( polynomial &Pc,polynomial &Pa, polynomial &Pb) {// 利用两个多项式的结点构成“和多项式” Pc = Pa+Pb… …if (DelAfter(Pa, e1)) a=e1.expn else a=MAXE;if (DelAfter(Pb, e2)) b=e2.expn else b=MAXE;while (!(a=MAXE && b=MAXE)) {… …}… …} // AddPolynswitch (*cmp(e1, e2)) {case -1: { // 多项式PA中当前结点的指数值小… … break; }case 0: { // 两者的指数值相等e1.coef= a.coef + b.coef ;if ( a.coef != 0.0 ) InsAfter(Pc, e1);… … break;}case 1: { //多项式PB中当前结点的指数值小… … break; }}本章小结1.了解线性表的逻辑结构特性是数据元素之间存在着线性关系,在计算机中表示这种关系的两类不同的存储结构是顺序存储结构和链式存储结构。用前者表示的线性表简称为顺序表,用后者表示的线性表简称为链表。2.熟练掌握这两类存储结构的描述方法,以及线性表的各种基本操作的实现。3.能够从时间和空间复杂度的角度综合比较线性表两种存储结构的不同特点及其适用场合。Quick Quiz(2)一、基础知识题1. 试比较顺序表与链表的优缺点。2. 试分析单链表与双链表的优缺点。3. 为什么在单循环链表中设置尾指针比设置头指针更好?4. 写出在循环双链表中的p所指结点之后插入一个s所指结点的操作。5. 写出在单链表中的p所指结点之前插入一个s所指结点的操作。6. 请利用链表来表示下面一元多项式二、算法设计题1. 有一个有n个结点的单链表,设计一个函数将第i-1个结点与第i个结点互换,但指针不变。2. 设计一个函数,查找单链表中数值为x的结点。3. 已知一个单链表,编写一个删除其值为x的结点的前趋结点的算法。4. 已知一个单链表,编写一个函数从此单链表中删除自第i个元素起的length个元素。5. 已知一个递增有序的单链表,编写一个函数向该单链表中插入一个元素为x的结点,使插入后该链表仍然递增有序。6. 已知一个单链表,编写一个函数将此单链表复制一个拷贝。7. 有一个共10个结点的单链表,试设计一个函数将此单链表分为两个结点数相等的单链表。8. 与上题相同的单链表,设计一个函数,将此单链表分成两个单链表,要求其中一个仍以原表头指针head1作表头指针,表中顺序包括原线性表的第一、三等奇数号结点;另一个链表以head2为表头指针,表中顺序包括原单链表第二、四等偶数号结点。9. 已知一个指针p指向单循环链表中的一个结点,编写一个对此单循环链表进行遍历的算法。10. 已知一个单循环链表,编写一个函数,将所有箭头方向取反。11. 在双链表中,若仅知道指针p指向某个结点,不知头指针,能否根据p遍历整个链表?若能,试设计算法实现。12. 试编写一个在循环双向链表中进行删除操作的算法,要求删除的结点是指定结点p的前趋结点。返回第二章习题解答一、基本知识题答案1. 答:顺序表用结点物理位置的相邻性来反映结点间的逻辑关系,其优点是:节省存储、随机存取,当表长变化较小,主要操作是进行查找时,宜采用顺序表。链表用附加的指针来反映结点间的逻辑关系,插入和删除操作相对比较方便,当表长变化较大,主要操作是进行插入和删除时,宜采用链表。2. 答:双链表比单链表多增加了一个指针域以指向结点的直接前趋,它是一种对称结构,因此在已知某个结点之前或之后插入一个新结点、删除该结点或其直接后继都同样方便,操作的时间复杂度为O(1);而单链表是单向结构,对于找一个结点的直接前趋的操作要从开始结点找起,其时间复杂度为O(n)。3. 答:由于对表的操作常常在表的两端进行,所以对单循环链表,当知道尾指针rear后,其另一端的头指针是rear->next->next(表中带头结点)。这会使操作变的更加容易。4. 答:s->prior=p;s->next=p->next;p->next->prior=s;p->next=s;5. 答:s->next=p->next;p->next=s;temp=p->data;p->data=s->data;s->data=temp;6. 答:多项式A(x)用链表表示如下:二、算法设计题答案1. 解:本题的算法思想是:要使结点互换而指针不变,只要将两个结点的数据进行交换即可。实现本题功能的函数如下:void exchange(node *head,int i,n){node *p,*q;int data;if(i>n)printf("error!\n");else{p=head;for(int j=1;jp=p->next;q=p->next;data=q->data;q->data=p->data;p->data=data;}}2. 解:实现本题功能的函数如下:void search(node *head,int x){node *p;p=head;while(p->data!=x && p!=NULL)p=p->next;if(p!=NULL)printf("结点找到了!\n");elseprintf("结点未找到!\n");}3. 解:本题的算法思想是:先找到值为x的结点*p和它的前趋结点*q,要删除*q,只需把*p的值x放到*q的值域中,再删除结点*p即可。实现本题功能的函数如下:void delete(node *head,int x){ node *p,*q;q=head;p=head->next;while((p!=NULL) && (p->data!=x)){ q=p;p=p->next;}if(p==NULL)printf("未找到x!\n");else if(q==head)printf("x为第一个结点,无前趋!\n");else{q->data=x;q->next=p->next;free(p);}}4. 解:实现本题功能的函数如下:void deletelength(node *head,int i,int length){ node *p,*q;int j;if(i==1)for(j=1;j<=length;j++) /*删除前k个元素*/{ p=head; /*p指向要删除的结点*/head=head->next;free(p);}else{ p=head;for(j=1;j<=i-2;j++)p=p->next;/*p指向要删除的结点的前一个结点*/for(j=1;j<=length;j++){q=p->next; /*q指向要删除的结点*/p->next=q->next;free(q);}}}5. 解:本题算法的思想是:先建立一个待插入的结点,然后依次与链表中的各结点的数据域比较大小,找到插入该结点的位置,然后插入该结点。实现本题功能的函数如下:void insert(node *head,int x){node *s,*p,*q;s=(node *)malloc(sizeof(node));/*建立一个待插入的结点*/s->data=x;s->next=NULL;if(head==NULL||xdata)/*若单链表为空或x小于第一个结点data域*/{s->next=head; /*插入到表头后面*/head=s;}else{q=head;p=q->next;while(p!=NULL && x>p->data){q=p;p=p->next;}s->next=p; /*插入结点*/q->next=s;}}6. 解:本题算法的思想是依次查找原单链表(其头指针为head1)中的每个结点,对每个结点复制一个新结点并链接到新的单链表(其头指针为head2)中。实现本题功能的函数如下:void copy(node *head1,node *head2){node *p,*q,*s;head2=(node *)malloc(sizeof(node));q=head2;q->data=head1->data;p=head1->next;while(p!=NULL){s=(node *)malloc(sizeof(node));s->data=p->data;q->next=s;q=s;p=p->next;}q->next=NULL;}7. 解:本题的算法思想是:在原单链表一半处将其分开,第5个结点的next域置为空,为第一个单链表的表尾。第二个单链表的表头指针指向原单链表的第6个结点。实现本题功能的函数如下,函数返回生成的第二个单链表的表头指针,第一个单链表仍然使用原单链表的表头指针。node* divide(node *head1){node *head2,*prior;head2=head1;for(int i=1;i<=5;i++){prior=head2;head2=head2->next;}prior->next=NULL;return head2;}8. 解:本题算法的思想是将第一个链表中的所有偶数序号的结点删除,同时把这些结点链接起来构成第二个单链表。实现本题功能的函数如下:void split(node* head1,node * head2){node *temp,*odd,*even;odd=head1;head2=head1->next;temp=head2;while(odd!=NULL && odd->next!=NULL){even=odd->next; /* even指向偶数序号的结点*/odd->next= even ->next;temp->next= even;temp= even;odd=odd->next; /*odd指向奇数序号的结点*/}even ->next=NULL;}9. 解:本题的算法思想是:因为是单循环链表,所以只要另设一指针q指向p用来帮助判断是否已经遍历一遍即可。实现本题功能的函数如下:void travel(node *p){ node *q=p;while(q->next!=p){printf("%4d",q->data);q=q->next;}printf("%4d",q->data);}10. 解:本题算法的思想是:从头到尾扫描该循环链表,将第一个结点的next域置为NULL,将第二个结点的next域指向第一个结点,如此直到最后一个结点,便用head指向它。由于是循环链表,所以判定最后一个结点时不能用p->next=NULL作为条件,而是用q指向第一个结点,以p!=q作为条件。实现本题功能的函数如下:void invert(node *head){node *p,*q,*r;p=head;q=p->next;while(p!=q){r=head;while(r->next!=p)r=r->next;p->next=r;p=p->next;}q->next=head;}11. 解:能。本题的算法思想是:分别从p开始沿着next以及prior向右、向左扫描直至各自的链为空即可遍历整个链表。实现本题功能的函数如下:void travellist(node *p){ node *q;q=p;while(q!=NULL){printf("%4d",q->data);q=q->next;}q=p->prior;while(q!=NULL){printf("%4d",q->data);q=q->prior;}}12. 解:实现本题功能的算法如下:void deleteprior(node *p){node *pri,q;pri=p->prior;q=pri->prior;if(pri==p)printf("p结点无前趋!\n");else{q->next=pri->next;p->prior=pri->prior;free(prior);}}返回(共65张PPT)第十二章文 件12.1 有关文件的基本概念12.2 顺 序 文 件12.3 索 引 文 件12.4 索 引 顺 序 文 件12.5 直 接 存 取 文 件12.6 多 关 键 字 文 件12.1 有关文件的基本概念一、文件即为记录的集合,和“查找表”的差别在于,“文件”指的是存储在外存储器中的记录的集合。记录是文件中可以存取的数据的基本单位。二、文件可按其中记录的类型不同而分成两类:其一为操作系统的文件,文件中的记录仅是一个字符组。由于操作系统中的文件仅是一维的连续字符序列,为了用户存取和加工的方便,将文件中的信息划分为若干组,其中每一组信息称作一个记录;其二为数据库文件,文件中的记录带有结构,是数据项的集合。记录是文件中可以存取的数据基本单位,数据项是文件中可以使用的数据最小单位三、记录中能识别不同记录的数据项被称为关键字,若该数据项能唯一识别一个记录,则称为主关键字,若能识别多个记录则称为次关键字。四、文件的逻辑结构指的是呈现在用户面前的文件中记录之间的逻辑关系;文件的物理结构指的是文件中的逻辑记录在存储器中的组织方式。五、文件的操作:检 索修 改排 序1.检索顺序存取:存取“当前记录的”下一个记录;直接存取:存取第i个记录;按关键字存取:存取其关键字等于给定值的记录。2.修改往文件中插入一个或一批记录;更新文件中某个记录的属性从文件中删除一个或一批记录;文件的操作方式可以实时处理或批量处理3.排序本章讨论文件的几种常见的物理结构。顺序文件索引文件索引顺序文件直接存取文件多关键字文件12.2 顺 序 文 件结 构 特 点:记录在文件中的排列顺序是由记录进入存储介质的次序决定的, 即文件物理结构中记录的排列顺序和文件的逻辑结构中记录的排列顺序一致.顺序文件的具体组织形式有两种:串联文件:物理记录之间的顺序由指针相链。连续文件:次序相继的两个物理记录其存储位置相邻;操作特点:1.便于进行顺序存取;2.不便于进行直接存取,为取第i个记录,必须先读出前i-1个记录,对于磁盘上的等长记录的连续文件可以进行折半查找;3.插入新的记录只能加在文件的末尾;4.删除记录时,只作标记;5.更新记录必须生成新的文件。顺序文件的插入、删除和更新操作在多数情况下都采用批处理方式。此时,为处理方便,通常将顺序文件作成有序文件,称作“主文件”,同时将所有的操作作成一个“事务文件”(经过排序也成为有序文件),所谓“批处理”,就是将这两个文件“合”为一个新的主文件。具体操作相当于“归并两个有序表”,(1)对于事务文件中的每个操作首先要判别其“合法性”;(2)事务文件中可能存在多个操作是对主文件中同一个记录进行的但有两点不同:假设主文件中含有n个记录,事务文件中含有m个记录,则对事务文件进行排序的时间复杂度为O(mlogm);内部归并的时间复杂度为O(m+n),则总的内部处理的时间为O(mlogm+n);批处理的时间分析:假设对外存进行一次读/取为s个记录,则整个批处理过程中读/写外存的次数为2 ( m/s + (m+n)/s )(其中s为对外存进行一次读/取的记录数)12.3 索 引 文 件一、结构特点:1.索引文件由“主文件”和多级“索引”组成。2.索引中的每个记录由“关键字”和“指针”组成。3.通常,索引文件中的主文件是无序文件,索引是 (按关键字有序)的有序文件。4.“索引”是在输入数据建立文件时自动生成。初建时的“静态索引”为无序文件,经过排序后成为有序文件。二、操作的特点:1.检索方式为:直接存取和按关键字存取。“按关键字检索”将分两步进行:先查索引,然后根据索引中指针所指索取记录。2.插入记录时,“记录”插入在主文件的末尾,而相应的“索引项”必须插入在索引的合适位置上。因此,最好在建索引表时留有一定“空位”。3.删除记录时,仅需删除索引表中相应的索引项即可。4.更新记录时,应将更新后的记录插入在主文件的末尾,同时修改相应的索引项。1.多 级 静 态 索 引2.动 态 索 引1.多级静态索引主 文 件索 引 表查 找 表第 二 查 找表第三查找表… ...… ...… ...… ...此时的索引文件结构:对主文件中每个记录建立一个索引项:主关键字 记录在主文件中的存储位置称作稠密索引,由这些索引项构成索引表;从索引表建立的索引称查找表,其中每个索引项为:最大关键字 其所在数据块的存储位置称这类索引为非稠密索引。类似地,由查找表建立的索引为第二查找表;由第二查找表建立的索引为第三查找表。按关键字进行检索时,从第三查找表开始,至多访问外存五次。索引表采用查找树表或哈希表。优点:1)不需要建立多级索引;2)初建索引不需要进行排序;3)插入或删除记录时,修改索引方便;2.动态索引用查找树表作索引时,查找索引所需访问外存次数的最大值恰为查找树的深度。稠密索引的优点是,可以实现“预查找”缺点是,索引表占用的存储空间大。可以作索引的树表有:二叉排序树、B-树和键树12.4 索 引 顺 序 文 件主文件按主关键字有序,对一组记录建立一个索引项(建立非稠密索引)。结构特点:一、ISAM文件ISAM(Index Sequential Access Method)(索引顺序存取方法)是一种专为磁盘存取设计的文件组织方法。有两种典型的索引顺序文件:1.文件的组织方式:主文件按柱面集中存放,同时建立三级索引:磁道索引、柱面索引和主索引。关键字 指针 关键字 指针磁道索引结构基本索引项溢出索引项2101024主索引r(14) r(21) r(38)r(41) r(57) r(63)r(72) r(85) r(99)溢 出 区磁 道 索 引r(514) … …溢 出 区磁道索引… … r(1024)一 个 柱 面….柱面索引992101024T0T1T2T3T4T52.操作的特点:检 索插入删除检索:可有两种方式:按关键字存取— 从主索引开始,到柱面索引,到磁道索引,最后取得记录,先后访问四次外存。顺序存取— 依关键字最小至大顺序存取插入:修改本磁道的索引项(包括基本索引项和溢出索引项)。将该磁道上关键字最大的记录移出到本柱面的溢出区中;将记录插入在某个磁道的合适位置上;删除:在被删记录当前存储位置上作“删除标记”。3.文件重组在经过多次的插入和删除操作之后,大量的记录进入文件的“溢出区”,而“基本存储区”中出现很多已被删去的记录空间,此时的文件结构很不合理。因此,对ISAM文件, 需要周期地进行重整。4.柱面索引的位置ISAM文件占有多个柱面,其柱面索引本身占有一个柱面,为使“磁头”的平均移动距离最小,柱面索引应设在数据文件所占全部柱面的中间位置上。二、VSAM文件VSAM(Vistual Storage Access Method)文件是利用操作系统中提供的虚拟存储器的功能组织的文件,免除了用户为读/写记录时直接对外存进行的操作,对用户而言,文件只有控制区间和控制区域等逻辑存储单位。… ............索引集B+树顺序集控制区域控制区间数据集1。文件的结构2. 控制区间是用户进行一次存取的逻辑单位,可看成是一个逻辑磁道。但它的实际大小和物理磁道无关。VSAM文件初建时,每个控制区间内的记录数不足额定数,并且有的控制区间内的记录数为零。控制区域由若干控制区间和它们的索引项组成,可看成是一个逻辑柱面.3.顺序集本身是一个单链表,它包含文件的全部索引项,同时,顺序集中的每个结点即为B+树的叶子结点,索引集中的结点即为B+树的非叶结点。4.文件的操作检索:可进行顺序存取和按关键字存取;插入:按关键字大小插入在某个适当的控制区间中,当控制区间中的记录数超过文件规定的大小时,要“分裂”控制区间,必要时,还需要“分裂”控制区域;删除:必须“真实地”删除记录,因此要在控制区间内“移动”记录;5.VSAM文件通常被作为大型索引顺序文件的标准组织方式。其缺点是:占有较多的存储空间,一般只能保持约75%的存储空间利用率。(因此,一般情况下,极少产生需要分裂控制区域的情况)其优点是:动态地分配和释放空间, 不需要重组文件;能较快地实现对“后插入”的记录的检索;12.5 直 接 存 取 文 件1.和前几节讨论的文件组织方法不同,直接存取文件的特点是,由记录的关键字“直接”得到记录在外存上的映象地址。类似于哈希表的构造方法,根据文件中关键字的特点设计一种“哈希函数”和“处理冲突的方法”将记录散列到外存储设备上,又称“散列文件”。2.哈希文件的结构由于记录在外存上是成组存放的,因此允许多个记录映象到同一个地址上。在此,称外存储器中存放多个记录的“数据块”为“桶”。 因此由哈希函数得到的映象地址为“桶地址”。例如:有一组关键字如下所列{589,063,269,505,764,182,166,330}假设哈希函数为 key MOD 7,每个桶可以容纳3个记录(称桶的容量为3),则哈希文件如下:基桶063 182589 505 764269166330溢出桶在哈希文件中,“冲突”和“溢出”是不同的概念。一般情况下,假设桶的大小为m,则允许哈希地址产生m-1次的冲突,当发生第m次冲突时,才需要进行“冲突处理”,对散列文件而言,通常采用链地址法出路冲突。为区别起见,称直接“散列”的数据块为“基桶”,而因“溢出”存放的数据块为“溢出桶”。3.文件的操作检索:只能进行按关键字的查找,不能进行顺序查找。检索时,先在基桶内进行查找,若不存在,则再到溢出桶中进行查找。插入:当查找不成功时,将记录插入在相应的基桶或溢出桶内。删除:对被删记录作特殊标记。4.优点:记录随机存放,不需要进行排序;插入、删除方便,存取速度快;节省存储空间,不需要索引区。缺点:不能进行顺序存取;在经过多次插入和删除操作之后,需进行“重组文件”的操作。12.6 多 关 键 字 文 件一、多关键字文件的特点除需要对主关键字建立“主索引”外, 尚需对各个次关键字建立“次索引”。次索引项: 次关键字 (指向记录的)指针二、次索引的组织方法1.多重链表文件 特点:将所有具有相同次关键字的记录链接在同一链表中,该链表的头指针即为次索引项中“指针域”的值。2.倒排文件 特点:将所有具有相同次关键字的记录构成一个次索引顺序表,此时的次索引顺序表中仅存放记录的“主关键字”或记录的“物理记录号”。次索引项中的“指针”指向相应的次索引顺序表。3.次关键字索引表本身的结构 可以是顺序表,也可以是树表或哈希表,视具体的次关键字的特性而定。本章学习要求:熟悉各类文件的特点,构造方法以及如何实现检索,插入和删除等操作。(共87张PPT)数据结构主讲 陈自刚E-mail: zgchen@Tel: 133236891601.1 数据结构讨论的范畴1.2 基本概念1.3 算法和算法的量度知 识 点数据结构中常用的基本概念和术语算法描述和分析方法难 点算法复杂性的分析方法要 求了解数据的逻辑结构和物理结构,算法的基本概念,它们对于程序设计的重要性以及相互关系掌握算法复杂性的概念及分析方法1.1 数据结构讨论的范畴Niklaus WirthAlgorithm + Data Structures = Programs程序设计:算法:数据结构:为计算机处理问题编制一组指令集处理问题的策略问题的数学模型非数值计算的程序设计问题例一: 求一组(n个)整数中的最大值算法: 模型:?基本操作是“比较两个数的大小”取决于整数值的范围例二:计算机对弈算法: 模型: 对弈的规则和策略棋盘及棋盘的格局例三:铺设城市的煤气管道算法: 模型: 如何规划使得总投资花费最少?图概括地说,数据结构是一门讨论“描述现实世界实体的数学模型(非数值计算)及其上的操作在计算机中如何表示和实现”的学科。1.2 基本概念一、数据与数据结构二、数据类型三、抽象数据类型一、数据与数据结构所有能被输入到计算机中,且能被计算机处理的符号(数值、字符等)的集合。数据:是计算机操作的对象的总称。是计算机处理的信息的某种特定的符号表示形式。是数据(集合)中的一个“个体”,在计算机中通常作为一个整体进行考虑和处理。是数据结构中讨论的基本单位。数据元素:如:整数“5”,字符“N”等。----是不可分割的“原子”其中每个款项称为一个“数据项”它是数据结构中讨论的最小单位数据元素也可以由若干款项构成。例如:描述一个学生的数据元素称之为组合项年 月 日姓 名学 号班 号性别出生日期入学成绩原子项数据结构:带结构的数据元素的集合有一个特性相同的数据元素的集合,如果在数据元素之间存在一种或多种特定的关系,则称为一个数据结构。指的是数据元素之间存在的关系例如: 用三个4位的十进制数表示一个含 12 位数的十进制数。3214,6587,9345 ─ a1(3214),a2(6587),a3(9345)则在数据元素 a1、a2 和 a3 之间存在着“次序”关系 a1,a2 、 a2,a3 3214,6587,9345a1 a2 a36587,3214,9345a2 a1 a3≠例如:又例,在2行3列的二维数组{a1, a2, a3, a4, a5, a6}中六个元素之间存在两个关系:行的次序关系:列的次序关系:row = {,,,}col = {,,}a1 a3 a5a2 a4 a6a1 a2 a3a4 a5 a6a1 a2 a3a4 a5 a6在一维数组 {a1, a2, a3, a4, a5, a6} 的 6 个数据元素之间存在如下的次序关系:{| i=1, 2, 3, 4, 5}数据结构是相互之间存在着某种逻辑关系的数据元素的集合。可见,不同的“关系”构成不同的“结构”再例,从关系或结构分,数据结构可归结为以下四类:线性结构树形结构图状结构集合结构数据结构包括“逻辑结构” 和“物理结构”两个方面(层次):逻辑结构 是对数据元素之间的逻辑关系的描述,它可以用一个数据元素的集合和定义在此集合上的若干关系来表示;物理结构 是逻辑结构在计算机中的表示和实现,故又称“存储结构” 。数据结构的形式定义描述为:数据结构是一个二元组Data_Structures = (D, S)其中:D 是数据元素的有限集,S 是 D上关系的有限集。数据的存储结构—— 逻辑结构在存储器中的映象“数据元素”的映象 ?“关系”的映象 ?数据元素的映象方法:用二进制位(bit)的位串表示数据元素(321)10 = (501)8 = (101000001)2A = (101)8 = (001000001)2关系的映象方法:(表示 x, y 的方法)顺序映象以相对的存储位置表示后继关系例如:令 y 的存储位置和 x 的存储位置之间差一个常量 C是一个隐含值,整个存储结构中只含数据元素本身的信息而 Cx y链式映象以附加信息(指针)表示后继关系需要用一个和 x 在一起的附加信息指示 y 的存储位置y x在不同的编程环境中,存储结构可有不同的描述方法,当用高级程序设计语言进行编程时,通常可用高级编程语言中提供的数据类型描述之。例如:以三个带有次序关系的整数表示一个长整数时,可利用 C 语言中提供的整数数组类型,typedef int Long_int [3]定义长为整数:typedef struct {int y; // 年号 Yearint m; // 月号 Monthint d; // 日号 Day} DateType; // 日期类型定义“日期”为:定义“学生”为:typedef struct {char id[8]; // 学号char name[16]; // 姓名char sex; // 性别‘M/F’:男/女DateType bdate; // 出生日期} Student; // 学生类型二、数据类型在用高级程序语言编写的程序中,必须对程序中出现的每个变量、常量或表达式,明确说明它们所属的数据类型。例如,C 语言中提供的基本数据类型有:整型 int浮点型 float字符型 char逻辑型bool ( C++语言)双精度型 double实型( C++语言)数据类型 是一个 值的集合和定义在此集合上的 一组操作的总称。不同类型的变量,其所能取的值的范围不同,所能进行的操作不同。三、抽象数据类型(Abstract Data Type 简称ADT)是指一个数学模型以及定义在此数学模型上的一组操作例如:“整数”是一个抽象数据类型。其数学特性和具体的计算机或语言无关。“抽象”的意义在于强调数据类型的数学特性。抽象数据类型还包括用户在设计软件系统时自己定义的数据类型。在构造软件系统的各个相对独立的模块时,定义一组数据和施与这些数据之上的一组操作,并在模块内部给出它们的表示和实现细节,在模块外部使用的只是抽象的数据和抽象的操作。例如,定义抽象数据类型“复数”数据对象:D={e1,e2|e1,e2∈RealSet }数据关系:R1={ | e1是复数的实数部分,| e2 是复数的虚数部分 }ADT Complex {基本操作:AssignComplex( &Z, v1, v2 )操作结果:构造复数 Z,其实部和虚部分别被赋以参数 v1 和 v2 的值。DestroyComplex( &Z)操作结果:复数Z被销毁。GetReal( Z, &realPart )初始条件:复数已存在。操作结果:用realPart返回复数Z的实部值。GetImag( Z, &ImagPart )初始条件:复数已存在。操作结果:用ImagPart返回复数Z的虚部值。Add( z1,z2, &sum )初始条件:z1, z2是复数。操作结果:用sum返回两个复数z1, z2 的和值。} ADT Complex假设:z1和z2是上述定义的复数则 Add(z1, z2, z3) 操作的结果z3 = z1 + z2即为用户企求的结果ADT 有两个重要特征:数据抽象用ADT描述程序处理的实体时,强调的是其本质的特征、其所能完成的功能以及它和外部用户的接口(即外界使用它的方法)数据封装将实体的外部特性和其内部实现细节分离,并且对外部用户隐藏其内部实现细节抽象数据类型的描述方法抽象数据类型可用(D,S,P)三元组表示其中,D 是数据对象,S 是 D 上的关系集,P 是对 D 的基本操作集。ADT 抽象数据类型名 {数据对象:〈数据对象的定义〉数据关系:〈数据关系的定义〉基本操作:〈基本操作的定义〉} ADT 抽象数据类型名其中基本操作的定义格式为:基本操作名(参数表)初始条件:〈初始条件描述〉操作结果:〈操作结果描述〉赋值参数 只为操作提供输入值;引用参数 以&打头,除可提供输入值外,还将返回操作结果。初始条件 描述了操作执行之前数据结构和参数应满足的条件,若不满足,则操作失败,并返回相应出错信息。操作结果 说明了操作正常完成之后,数据结构的变化状况和应返回的结果。若初始条件为空,则省略之。抽象数据类型的表示和实现抽象数据类型需要通过固有数据类型(高级编程语言中已实现的数据类型)来实现。例如,对以上定义的复数typedef struct {float realpart;float imagpart;}complex;// -----存储结构的定义// -----基本操作的函数原型说明void Assign( complex &Z,float realval, float imagval );// 构造复数 Z,其实部和虚部分别被赋以参数 // realval 和 imagval 的值float GetReal( cpmplex Z );// 返回复数 Z 的实部值float Getimag( cpmplex Z );// 返回复数 Z 的虚部值void add( complex z1, complex z2,complex &sum );// 以 sum 返回两个复数 z1, z2 的和// -----基本操作的实现void add( complex z1, complex z2,complex &sum ) {// 以 sum 返回两个复数 z1, z2 的和sum.realpart = z1.realpart + z2.realpart;sum.imagpart = z1.imagpart + z2.imagpart;}{ 其它省略 }1.3 算法和算法的衡量一、算法二、算法设计的原则三、算法效率的衡量方法和准则四、算法的存储空间需求算法是为了解决某类问题而规定的一个有限长的操作序列。一个算法必须满足以下五个重要特性:1.有穷性 2.确定性 3.可行性4.有输入 5.有输出一、算法1.有穷性 对于任意一组合法输入值,在执行有穷步骤之后一定能结束,即:算法中的每个步骤都能在有限时间内完成;2.确定性 对于每种情况下所应执行的操作,在算法中都有确切的规定,使算法的执行者或阅读者都能明确其含义及如何执行。并且在任何条件下,算法都只有一条执行路径;3.可行性 算法中的所有操作都必须足够基本,都可以通过已经实现的基本操作运算有限次实现之;4.有输入 作为算法加工对象的量值,通常体现为算法中的一组变量。有些输入量需要在算法执行过程中输入,而有的算法表面上可以没有输入,实际上已被嵌入算法之中;5.有输出 它是一组与“输入”有确定关系的量值,是算法进行信息加工后得到的结果,这种确定关系即为算法的功能。二、算法设计的原则设计算法时,通常应考虑达到以下目标:1.正确性2. 可读性3.健壮性4.高效率与低存储量需求1.正确性首先,算法应当满足以特定的“规格说明”方式给出的需求。其次,对算法是否“正确”的理解可以有以下四个层次:a.程序中不含语法错误;b.程序对于几组输入数据能够得出满足要求的结果;c.程序对于精心选择的、典型、苛刻且带有刁难性的几组输入数据能够得出满足要求的结果;通常以第 c 层意义的正确性作为衡量一个算法是否合格的标准。d.程序对于一切合法的输入数据都能得出满足要求的结果;2. 可读性算法主要是为了人的阅读与交流,其次才是为计算机执行。因此算法应该易于人的理解;另一方面,晦涩难读的程序易于隐藏较多错误而难以调试;3.健壮性当输入的数据非法时,算法应当恰当地作出反映或进行相应处理,而不是产生莫名奇妙的输出结果。并且,处理出错的方法不应是中断程序的执行,而应是返回一个表示错误或错误性质的值,以便在更高的抽象层次上进行处理。4.高效率与低存储量需求通常,效率指的是算法执行时间;存储量指的是算法执行过程中所需的最大存储空间。两者都与问题的规模有关。算法的描述本书将采用类C语言描述算法类C语言是标准C语言的简化 ,与标准C语言的主要区别如下:1. 所有算法都以如下所示的函数形式表示:函数类型 函数名(参数表){语句序列 }类C语言的形参书写比标准C语言简单,如,int xyz(int a,int b,int c)可以简单写成int xyz (int a,b,c)类C与标准C的主要区别(续)2. 局部量的说明可以省略,必要时对其作用给予注释 。3. 不含go to语句,增加一个出错处理语句error(字符串),其功能是终止算法的执行并给出表示出错信息的字符串。4. 输入/输出语句有:输入语句 scanf([格式串]),变量1,…,变量N);输出语句 printf([格式串]),变量1,…,变量N);通常省略格式串 。返回三、算法效率的衡量方法和准则通常有两种衡量算法效率的方法:事后统计法事前分析估算法缺点:1。必须执行程序2。其它因素掩盖算法本质和算法执行时间相关的因素:1.算法选用的策略2.问题的规模3.编写程序的语言4.编译程序产生的机器代码的质量5.计算机执行指令的速度一个特定算法的“运行工作量”的大小,只依赖于问题的规模(通常用整数量n表示),或者说,它是问题规模的函数。假如,随着问题规模 n 的增长,算法执行时间的增长率和 f(n) 的增长率相同,则可记作:T (n) = O(f(n))称T (n) 为算法的(渐近)时间复杂度算法复杂性的分析算法的复杂性包括时间复杂性(所需运算时间)和空间复杂性(所占存储空间),重点是时间复杂性 。一个算法所需的运算时间通常与所解决问题的规模大小有关。用n 表示问题规模的量 ,把算法运行所需的时间T表示为n的函数,记为T(n)。不同的T(n)算法,当n增长时,运算时间增长的快慢很不相同。一个算法所需的执行时间就是该算法中所有语句执行次数之和。渐进时间复杂性:当n逐渐增大时T(n)的极限情况,一般简称为时间复杂性。时间复杂性常用数量级的形式来表示,记作T(n)=O(f(n))。其中,大写字母O为Order(数量级)的字头,f(n)为函数形式,如T(n)=O(n2)。当T(n)为多项式时,可只取其最高次幂项,且它的系数也可略去不写。一般地,对于足够大的n,常用的时间复杂性存在以下顺序:O(1)< O(logn)< O(n)< O(n*logn)< O(n2)< O(n3)…其中,O(1)为常数数量级,即算法的时间复杂性与输入规模n无关。算法的运行时间往往还与具体输入的数据有关,通常用以下两种方法来确定一个算法的运算时间:平均时间复杂性:研究同样的n值时各种可能的输入,取它们运算时间的平均值。2. 最坏时间复杂性:研究各种输入中运算最慢的一种情况下的运算时间。返回如何估算算法的时间复杂度?算法 = 控制结构 + 原操作(固有数据类型的操作)算法的执行时间 =原操作(i)的执行次数×原操作(i)的执行时间算法的执行时间与原操作执行次数之和 成正比从算法中选取一种对于所研究的问题来说是 基本操作 的原操作,以该基本操作 在算法中重复执行的次数 作为算法运行时间的衡量准则。计算下面交换i和j内容程序段的时间复杂性。temp=i;i=j;j=temp;解:以上三条单个语句均执行1次,该程序段的执行时间是一个与问题n无关的常数,因此,算法的时间复杂度为常数阶,记作T(n)=O(1).计算下面求累加和程序段的时间复杂性(1) sum=0; (一次)(2) for(i=1;i<=n;i++) (n次 )(3) for(j=1;j<=n;j++) (n2次 )(4) sum++; (n2次 )解:T(n)=2n2+n+1 =O(n2)返回例一两个矩阵相乘void mult(int a[], int b[], int& c[] ) {// 以二维数组存储矩阵元素,c 为 a 和 b 的乘积for (i=1; i<=n; ++i)for (j=1; j<=n; ++j) {c[i,j] = 0;for (k=1; k<=n; ++k)c[i,j] += a[i,k]*b[k,j];} //for} //mult基本操作: 乘法操作时间复杂度: O(n3)例二选择排序void select_sort(int& a[], int n) {// 将 a 中整数序列重新排列成自小至大有序的整数序列。} // select_sort基本操作:比较(数据元素)操作时间复杂度: O(n2)j = i; // 选择第 i 个最小元素for ( k = i+1; k < n; ++k )if (a[k] < a[j] ) j = k;for ( i = 0; i< n-1; ++i ) {if ( j != i ) a[j] ←→ a[i]}例三起泡排序void bubble_sort(int& a[], int n) {// 将 a 中整数序列重新排列成自小至大有序的整数序列。for (i=n-1, change=TRUE; i>1 && change; --i)} // bubble_sort基本操作: 赋值操作时间复杂度: O(n2){ change = FALSE; // change 为元素进行交换标志for (j=0; jif (a[j] > a[j+1]){ a[j] ←→ a[j+1]; change = TRUE ;}} // 一趟起泡四、算法的存储空间需求算法的空间复杂度定义为:表示随着问题规模 n 的增大,算法运行所需存储量的增长率与 g(n) 的增长率相同。S(n) = O(g(n))算法的存储量包括:1.输入数据所占空间2.程序本身所占空间;3.辅助变量所占空间。若输入数据所占空间只取决与问题本身,和算法无关,则只需要分析除输入和程序之外的辅助变量所占额外空间。若所需额外空间相对于输入数据量来说是常数,则称此算法为原地工作。若所需存储量依赖于特定的输入,则通常按最坏情况考虑。1. 熟悉各名词、术语的含义,掌握基本概念。2. 理解算法五个要素的确切含义。本章学习要点3. 掌握计算语句频度和估算算法时间复杂度的方法。QUICK QUIZ(1)一、名词解释数据 数据项 数据元素 数据结构数据逻辑结构 数据物理结构 算法算法的时间复杂性二、简答1. 算法分析的目的是什么?2. 什么是算法的最坏和平均时间复杂性?三、分析下列算法的时间复杂性:1.sum=0;for (i=1;i<=n;i++){sum=sum+i;}2.i=1;while(i<=n)i=i*10; 1*103.sum=0;for(i=0;ifor(j=0;jsum=sum+Array[i][j];返回一、名词解释数据:就是一切能够由计算机接受和处理的对象。数据项:是数据的不可分割的最小单位,在有些场合下,数据项又称为字段或域。数据元素:是数据的基本单位,在程序中作为一个整体加以考虑和处理,也称为元素、顶点或记录。它可以由若干个数据项组成。数据结构:指的是数据之间的相互关系,即数据的组织形式,它包括数据的逻辑结构、数据的存储结构和数据的运算三个方面的内容。数据逻辑结构:是指数据元素之间的逻辑关系,是从逻辑上描述数据,与数据的存储无关,独立于计算机。数据物理结构:是指数据元素及其关系在计算机存储器内的表示,是数据的逻辑结构用计算机语言的实现,是依赖于计算机语言的。算法:是对特定问题求解步骤的一种描述。它是一个有穷的规则序列,这些规则决定了解决某一特定问题的一系列运算。由此问题相关的一定输入,计算机依照这些规则进行计算和处理,经过有限的计算步骤后能得到一定的输出。算法的时间复杂性:是该算法的时间耗费,它是该算法所求解问题规模n的函数。当n趋向无穷大时,我们把时间复杂性T(n)的数量级称为算法的渐进时间复杂性。二、简答题1. 答:对算法进行分析的目的有两个:第一个目的是可以从解决同一问题的不同算法中区分相对优劣,选出较为适用的一种;第二个目的是有助于设计人员考虑对现有算法进行改进或设计出新的算法。2. 答:算法的最坏时间复杂性是研究各种输入中运算最慢的一种情况下的运算时间;平均时间复杂性是研究同样的n值时各种可能的输入,取它们运算时间的平均值。三、答案1.答:该程序段的时间复杂性为T(n)=O(n)。2.答:该程序段的时间复杂性T(n)=O(log10n)。3.答:该程序段的时间复杂性T(n)=O(n2)。返回下载第2章 数据类型、运算符和表达式2.1 C语言的数据类型C语言有五种基本数据类型:字符、整型、单精度实型、双精度实型和空类型。尽管这几种类型数据的长度和范围随处理器的类型和 C语言编译程序的实现而异,但以 b i t为例,整数与C P U字长相等,一个字符通常为一个字节,浮点值的确切格式则根据实现而定。对于多数微机,表2 - 1给出了五种数据的长度和范围。表2-1 基本类型的字长和范围类 型 长 度(b i t) 范 围c h a r (字符型) 8 0 ~ 2 5 5i n t(整型) 1 6 - 3 2 7 6 8 ~ 3 2 7 6 7f l o a t(单精度型) 3 2 约精确到 6位数d o u b l e(双精度型) 6 4 约精确到 1 2位数v o i d(空值型) 0 无值表中的长度和范围的取值是假定 C P U的字长为1 6 b i t。C语言还提供了几种聚合类型(aggregate types),包括数组、指针、结构、共用体(联合)、位域和枚举。这些复杂类型在以后的章节中讨论。除v o i d类型外,基本类型的前面可以有各种修饰符。修饰符用来改变基本类型的意义,以便更准确地适应各种情况的需求。修饰符如下: signed(有符号)。 unsigned(无符号)。 long(长型符)。 short(短型符)。修饰符s i g n e d、s h o r t、l o n g和u n s i g n e d适用于字符和整数两种基本类型,而 l o n g还可用于d o u b l e(注意,由于 long float与d o u b l e意思相同,所以A N S I标准删除了多余的 long float)。表2 - 2给出所有根据A N S I标准而组合的类型、字宽和范围。切记,在计算机字长大于 1 6位的系统中, short int与signed char可能不等。表2-2 ANSI标准中的数据类型类 型 长 度(b i t) 范 围c h a r (字符型) 8 A S C I I字符unsigned char(无符号字符型) 8 0 ~ 2 5 5signed char(有符号字符型 ) 8 - 1 2 8 ~ 1 2 7i n t(整型) 1 6 3 2 7 6 8 ~ 3 2 7 6 7unsigned int(无符号整型 ) 1 6 0 ~ 6 5 5 3 5signed int(有符号整型 ) 1 6 同i n t第2章 数据类型、运算符和表达式 1 5下载(续)类 型 长 度(b i t) 范 围short int(短整型) 8 1 2 8 ~ 1 2 7unsigned short int(无符号短整型 ) 8 0 ~ 2 5 5signed short int(有符号短整型 ) 8 同s h o r t i n tlong int(长整型) 3 2 2 1 4 7 4 8 3 6 4 8 ~ 2 1 4 7 4 8 3 6 4 9signed long int(有符号长整型 ) 3 2 2 1 4 7 4 8 3 6 4 8 ~ 2 1 4 7 4 8 3 6 4 9unsigned long int(无符号长整型 ) 3 2 0 ~ 4 2 9 4 9 6 7 2 9 6f l o a t(单精度型) 3 2 约精确到6位数d o u b l e(双精度型) 6 4 约精确到1 2位数*表中的长度和范围的取值是假定 C P U的字长为1 6 b i t。因为整数的缺省定义是有符号数,所以 s i n g e d这一用法是多余的,但仍允许使用。某些实现允许将 u n s i g n e d用于浮点型,如 unsigned double。但这一用法降低了程序的可移植性,故建议一般不要采用。为了使用方便,C编译程序允许使用整型的简写形式: short int 简写为s h o r t。 long int 简写为l o n g。 unsigned short int 简写为unsigned short。 unsigned int 简写为u n s i g n e d。 unsigned long int 简写为unsigned long。即,i n t可缺省。2.2 常量与变量2.2.1 标识符命名在C语言中,标识符是对变量、函数标号和其它各种用户定义对象的命名。标识符的长度可以是一个或多个字符。绝大多数情况下,标识符的第一个字符必须是字母或下划线,随后的字符必须是字母、数字或下划线(某些 C语言编译器可能不允许下划线作为标识符的起始字符)。下面是一些正确或错误标识符命名的实例。正确形式 错误形式c o u n t 2 c o u n tt e s t 2 3 hi! thereh i g h _ b a l a n c e h i g h . . b a l a n c eA N S I标准规定,标识符可以为任意长度,但外部名必须至少能由前 8个字符唯一地区分。这里外部名指的是在链接过程中所涉及的标识符,其中包括文件间共享的函数名和全局变量名。这是因为对某些仅能识别前 8个字符的编译程序而言,下面的外部名将被当作同一个标识符处理。co u n t e r s co u n t e r s 1 co u n t e r s 2A N S I标准还规定内部名必须至少能由前 3 1个字符唯一地区分。内部名指的是仅出现于定1 6 C语言程序设计下载义该标识符的文件中的那些标识符。C语言中的字母是有大小写区别的,因此 count Count COUNT是三个不同的标识符。标识符不能和C语言的关键字相同,也不能和用户已编制的函数或 C语言库函数同名。2.2.2 常量C语言中的常量是不接受程序修改的固定值,常量可为任意数据类型,如下例所示 :数据类型 常量举例c h a r ' a '、' \ n '、' 9 'i n t 2 1、 123 、2100 、-2 3 4long int 3 5 0 0 0、 -3 4short int 1 0、-1 2、9 0unsigned int 1 0 0 0 0、 9 8 7、 4 0 0 0 0f l o a t 1 2 3 . 2 3、 4 . 3 4 e-3d o u b l e 1 2 3 . 2 3、 1 2 3 1 2 3 3 3、 -0 . 9 8 7 6 2 3 4C语言还支持另一种预定义数据类型的常量,这就是串。所有串常量括在双撇号之间,例如"This is a test"。切记,不要把字符和串相混淆,单个字符常量是由单撇号括起来的,如 ' a '。2.2.3 变量其值可以改变的量称为变量。一个变量应该有一个名字 (标识符 ),在内存中占据一定的存储单元,在该存储单元中存放变量的值。请注意区分变量名和变量值这两个不同的概念。所有的C变量必须在使用之前定义。定义变量的一般形式是:type variable_list;这里的t y p e必须是有效的C数据类型,v a r i a b l e _ l i s t(变量表)可以由一个或多个由逗号分隔的多个标识符名构成。下面给出一些定义的范例。int i, j, l;short int si;unsigned int ui;double balance, profit,loss;注意 C语言中变量名与其类型无关。2.3 整型数据2.3.1 整型常量整型常量及整常数。它可以是十进制、八进制、十六进制数字表示的整数值。十进制常数的形式是:di g i t s这里d i g i t s可以是从0到9的一个或多个十进制数位,第一位不能是 0。八进制常数的形式是:第2章 数据类型、运算符和表达式 1 7下载0d i g i t s在此,d i g i t s可以是一个或多个八进制数( 0~7之间),起始0是必须的引导符。十六进制常数是下述形式:0x h d i g i t s0X h d i g i t s这里h d i g i t s可以是一个或多个十六进制数(从 0~9的数字,并从“ a”~“ f”的字母)。引导符0是必须有的,X即字母可用大写或小写。注意,空白字符不可出现在整数数字之间。表 2 - 3列出了整常数的形式。表2-3 整常数的例子十 进 制 八 进 制 十 六 进 制1 0 0 1 2 0Xa或0XA1 3 2 0 2 0 4 0X8 43 2 1 7 9 0 7 6 6 6 3 0X7 d b 3或0X7 D B 3整常数在不加特别说明时总是正值。如果需要的是负值,则负号“ -”必须放置于常数表达式的前面。每个常数依其值要给出一种类型。当整常数应用于一表达式时,或出现有负号时,常数类型自动执行相应的转换,十进制常数可等价于带符号的整型或长整型,这取决于所需的常数的尺寸。八进制和十六进制常数可对应整型、无符号整型、长整型或无符号长整型,具体类型也取决于常数的大小。如果常数可用整型表示,则使用整型。如果常数值大于一个整型所能表示的最大值,但又小于整型位数所能表示的最大数,则使用无符号整型。同理,如果一个常数比无符号整型所表示的值还大,则它为长整型。如果需要,当然也可用无符号长整型。在一个常数后面加一个字母 l或L,则认为是长整型。如1 0 L、7 9 L、0 1 2 L、0 11 5 L、0 X A L、0 x 4 f L等。2.3.2 整型变量前面已提到, C规定在程序中所有用到的变量都必须在程序中指定其类型,即“定义”。这是和B A S I C、F O RT R A N不同的,而与P a s c a l相似。[例2 - 1 ]ma i n ( ){int a,b,c,d; /* 指定a, b , c , d 为整型变量*/unsigned u; /* 指定u为无符号整型变量*/a=12; b=-24; u=10;c=a+u; d=b+u;printf("a+u=%d, b+u=%d\n",c,d);}运行结果为 :RU N a+u=22, b+u=-141 8 C语言程序设计下载可以看到不同类型的整型数据可以进行算术运算。在本例中是 i n t型数据与unsingned int型数据进行相加减运算。2.4 实型数据2.4.1 实型常量实型常量又称浮点常量,是一个十进制表示的符号实数。符号实数的值包括整数部分、尾数部分和指数部分。实型常量的形式如下:[d i g i t s ] [ . d i g i t s ] [ E | e [ + | - ] d i g i t s ]在此d i g i t s是一位或多位十进制数字(从 0~9)。 E(也可用e)是指数符号。小数点之前是整数部分,小数点之后是尾数部分,它们是可省略的。小数点在没有尾数时可省略。指数部分用 E或e开头,幂指数可以为负,当没有符号时视为正指数的基数为 1 0,如1 . 5 7 5 E 1 0表示为:1 . 5 7 5×1 01 0。在实型常量中不得出现任何空白符号。在不加说明的情况下,实型常量为正值。如果表示负值,需要在常量前使用负号。下面是一些实型常量的示例:15.75, 1.575E10, 1575e-2, -0.0025, -2.5e-3, 25E-4所有的实型常量均视为双精度类型。实型常量的整数部分为 0时可以省略,如下形式是允许的:.57, .0075e2, -.125, -.175E。-2注意 字母E或e之前必须有数字,且E或e后面指数必须为整数,如e 3、2 . 1 e 3 . 5、. e 3、e等都是不合法的指数形式。2.4.2 实型变量实型变量分为单精度( f l o a t型)和双精度( d o u b l e型)。对每一个实型变量都应再使用前加以定义。如:float x,y; / *指定x , y为单精度实数* /double z; / *指定z为双精度实数* /在一般系统中,一个 f l o a t型数据在内存中占 4个字节( 3 2位)一个 d o u b l e型数据占 8个字节(6 4位)。单精度实数提供 7位有效数字,双精度提供 1 5 ~ 1 6位有效数字,数值的范围随机器系统而异。值得注意的是,实型常量是 d o u b l e型,当把一个实型常量赋给一个 f l o a t型变量时,系统会截取相应的有效位数。例如float a;a= 1 1 1 1 1 1 . 1 1 1 ;由于f l o a t型变量只能接收 7位有效数字,因此最后两位小数不起作用。如果将 a改为d o u b l e型,则能全部接收上述 9位数字并存储在变量 a中。第2章 数据类型、运算符和表达式 1 9下载2.5 字符型数据2.5.1 字符常量字符常量是指用一对单引号括起来的一个字符。如‘ a’,‘9’,‘!’。字符常量中的单引号只起定界作用并不表示字符本身。单引号中的字符不能是单引号(’)和反斜杠( \),它们特有的表示法在转义字符中介绍。在C语言中,字符是按其所对应的 A S C I I码值来存储的,一个字符占一个字节。例如:字符 A S C I I码值(十进制)! 3 30 4 81 4 99 5 7A 6 5B 6 6a 9 7b 9 8注意 字符' 9 '和数字9的区别,前者是字符常量,后者是整型常量,它们的含义和在计算机中的存储方式都截然不同。由于C语言中字符常量是按整数( s h o r t型)存储的,所以字符常量可以像整数一样在程序中参与相关的运算。例如:' a '-3 2 ; /* 执行结果9 7-32 = 65 * /'A' + 32; /* 执行结果65+32 = 97 * /' 9 '-9; /* 执行结果5 7-9 = 48 * /2.5.2 字符串常量字符串常量是指用一对双引号括起来的一串字符。双引号只起定界作用,双引号括起的字符串中不能是双引号( ")和反斜杠(\),它们特有的表示法在转义字符中介绍。例如:"C h i n a " ,"C program", "YES&NO", "33312-2341", 等"A。"C语言中,字符串常量在内存中存储时,系统自动在字符串的末尾加一个“串结束标志”,即A S C I I码值为 0的字符 N U L L,常用 \ 0表示。因此在程序中,长度为 n个字符的字符串常量,在内存中占有n + 1个字节的存储空间。例如,字符串 C h i n a有5个字符,作为字符串常量 " C h i n a "存储于内存中时,共占 6个字节,系统自动在后面加上 N U L L字符,其存储形式为:C h i n a N U L L要特别注意字符串与字符串常量的区别,除了表示形式不同外,其存储性质也不相同,字符' A '只占1个字节,而字符串常量 " A "占2个字节。2 0 C语言程序设计下载2.5.3 转义字符转义字符是C语言中表示字符的一种特殊形式。通常使用转义字符表示 A S C I I码字符集中不可打印的控制字符和特定功能的字符,如用于表示字符常量的单撇号( '),用于表示字符串常量的双撇号( ")和反斜杠( \)等。转义字符用反斜杠 \后面跟一个字符或一个八进制或十六进制数表示。表2 - 4给出了C语言中常用的转义字符。表2-4 转义字符转 义 字 符 意 义 A S C I I码值(十进制)\ a 响铃( B E L ) 0 0 7\ b 退格( B S ) 0 0 8\ f 换页( F F ) 0 1 2\ n 换行( L F ) 0 1 0\ r 回车( C R ) 0 1 3\ t 水平制表( H T ) 0 0 9\ v 垂直制表( V T ) 0 11\ \ 反斜杠 0 9 2\ 问号字符 0 6 3\ ' 单引号字符 0 3 9\ " 双引号字符 0 3 4\ 0 空字符( N U L L ) 0 0 0\ d d d 任意字符 三位八进制\ x h h 任意字符 二位十六进制字符常量中使用单引号和反斜杠以及字符常量中使用双引号和反斜杠时,都必须使用转义字符表示,即在这些字符前加上反斜杠。在C程序中使用转义字符 \ d d d或者\ x h h可以方便灵活地表示任意字符。 \ d d d为斜杠后面跟三位八进制数,该三位八进制数的值即为对应的八进制 A S C I I码值。\ x后面跟两位十六进制数,该两位十六进制数为对应字符的十六进制 A S C I I码值。使用转义字符时需要注意以下问题:1) 转义字符中只能使用小写字母,每个转义字符只能看作一个字符。2) \v 垂直制表和 \f 换页符对屏幕没有任何影响,但会影响打印机执行响应操作。3) 在C程序中,使用不可打印字符时,通常用转义字符表示。2.5.4 符号常量C语言允许将程序中的常量定义为一个标识符,称为符号常量。符号常量一般使用大写英文字母表示,以区别于一般用小写字母表示的变量。符号常量在使用前必须先定义,定义的形式是:#define <符号常量名> <常量>例如:#define PI 3. 1 4 1 5 9 2 6#define TRUE 1#definr FALSE 0#define STAR '* '这里定义 P I、T R U E、F L A S E、S TA R为符号常量,其值分别为 3 . 1 4 1 5 9 2 6,1,0, ' * '。第2章 数据类型、运算符和表达式 2 1下载# d e f i n e是C语言的预处理命令,它表示经定义的符号常量在程序运行前将由其对应的常量替换。定义符号常量的目的是为了提高程序的可读性,便于程序的调试和修改。因此在定义符号常量名时,应使其尽可能地表达它所代表的常量的含义,例如前面所定义的符号常量名P I (π),表示圆周率 3 . 1 4 1 5 9 2 6。此外,若要对一个程序中多次使用的符号常量的值进行修改,只须对预处理命令中定义的常量值进行修改即可。2.5.5 字符变量字符变量用来存放字符常量,注意只能存放一个字符,不要以为在一个字符变量中可以放字符串。字符变量的定义形式如下:char c1, c2;它表示c 1和c 2为字符变量,各放一个字符。因此可以用下面语句对 c 1、c 2赋值:c1 = 'a'; c2 = 'b';[例2 - 2 ]ma i n ( ){char c1,c2;c1 = 9 7 ; c2 = 9 8 ;printf("%c %c",c1,c2);}c 1、c 2被指定为字符变量。但在第 3行中,将整数 9 7和9 8分别赋给 c 1和c 2,它的作用相当于以下两个赋值语句:c1 = ' a ' ; c2 = ' b ' ;因为 ' a '和' b '的A S C I I码为9 7和9 8。第4行将输出两个字符。 " % c "是输出字符的格式。程序输出:RU N a b[例2 - 3 ]ma i n ( ){ char c1,c2;c1 = ' a ' ;c2 = ' b ' ;c1 = c1 - 32; c2 =c2 - 32;pr i n t f ( " % c %c " , c 1 , c 2 ) ;}运行结果为:RU N A B它的作用是将两个小写字母转换为大写字母。因为 ' a '的A S C I I码为9 7,而' A '为6 5,' b '为9 8,' B '为6 6。从A S C I I代码表中可以看到每一个小写字母比大写字母的 A S C I I码大3 2。即 'a'='A' +3 2。2 2 C语言程序设计下载2.6 运算符C语言的内部运算符很丰富,运算符是告诉编译程序执行特定算术或逻辑操作的符号。 C语言有三大运算符:算术、关系与逻辑、位操作。另外, C还有一些特殊的运算符,用于完成一些特殊的任务。2.6.1 算术运算符表2 - 5列出了 C语言中允许的算术运算符。在 C语言中,运算符“ +”、“-”、“*”和“ /”的用法与大多数计算机语言的相同,几乎可用于所有 C语言内定义的数据类型。当“ /”被用于整数或字符时,结果取整。例如,在整数除法中, 1 0 / 3 = 3。一元减法的实际效果等于用 - 1乘单个操作数,即任何数值前放置减号将改变其符号。模运算符“%”在C语言中也同它在其它语言中的用法相同。切记,模运算取整数除法的余数,所以“%”不能用于 f l o a t和d o u b l e类型。表2-5 算术运算符运 算 符 作 用 运 算 符 作 用- 减法,也是一元减法 % 模运算+ 加法 - - 自减(减 1)* 乘法 + + 自增(增 1)/ 除法下面是说明 %用法的程序段。int x,y;x= 1 0 ;y= 3 ;pr i n t f ( " % d " , x / y ) ; /* 显示 3 */pr i n t f ( " % d " , x % y ) ; /* 显示 1, 整数除法的余数 */x= 1 ;y= 2 ;pr i n t f ( " % d , % d " , x / y , x % y ) ; /* 显示 0,1 */最后一行打印一个0和一个1,因为1 / 2整除时为0,余数为1,故1 % 2取余数1。2.6.2 自增和自减C语言中有两个很有用的运算符,通常在其它计算机语言中是找不到它们的—自增和自减运算符, + +和- -。运算符“ + +”是操作数加1,而“- -”是操作数减1,换句话说:x= x + 1 ; 同++ x ;x= x - 1 ; 同-- x ;自增和自减运算符可用在操作数之前,也可放在其后,例如: x = x + 1;可写成 + + x;或x + +;但在表达式中这两种用法是有区别的。自增或自减运算符在操作数之前, C语言在引用第2章 数据类型、运算符和表达式 2 3下载操作数之前就先执行加 1或减1操作;运算符在操作数之后, C语言就先引用操作数的值,而后再进行加1或减1操作。请看下例:x= 1 0 ;y= + + x ;此时,y = 11。如果程序改为:x= 1 0 ;y= x + + ;则y = 1 0。在这两种情况下, x都被置为 11,但区别在于设置的时刻,这种对自增和自减发生时刻的控制是非常有用的。在大多数C编译程序中,为自增和自减操作生成的程序代码比等价的赋值语句生成的代码要快得多,所以尽可能采用加 1或减1运算符是一种好的选择。下面是算术运算符的优先级:最高 + +、- --(一元减)*、/、%最低 +、-编译程序对同级运算符按从左到右的顺序进行计算。当然,括号可改变计算顺序。 C语言处理括号的方法与几乎所有的计算机语言相同:强迫某个运算或某组运算的优先级升高。2.6.3 关系和逻辑运算符关系运算符中的“关系”二字指的是一个值与另一个值之间的关系,逻辑运算符中的“逻辑”二字指的是连接关系的方式。因为关系和逻辑运算符常在一起使用,所以将它们放在一起讨论。关系和逻辑运算符概念中的关键是 Tr u e(真)和F l a s e(假)。C语言中,非 0为Tr u e,0为F l a s e。使用关系或逻辑运算符的表达式对 Fl a s e和Tu r e分别返回值0或1 (见表2 - 6 )。表2-6 关系和逻辑运算符关系运算符 含 义 关系运算符 含 义> 大于 < = 小于或等于> = 大于等于 = = 等于< 小于 ! = 不等于逻辑运算符 含 义& & 与| | 或! 非表2 - 6给出于关系和逻辑运算符,下面用 1和0给出逻辑真值表。关系和逻辑运算符的优先级比算术运算符低,即像表达式 1 0 > 1 + 1 2的计算可以假定是对表达式1 0 > ( 1 + 1 2 )的计算,当然,该表达式的结果为 Fl a s e。在一个表达式中允许运算的组合。例如:10 > 5 & & ! ( 1 0 < 9 ) | | 3 < = 42 4 C语言程序设计下载p q p & & q p | | q ! p0 0 0 0 10 1 0 1 11 1 1 1 01 0 0 1 0这一表达式的结果为 Tr u e。下表给出了关系和逻辑运算符的相对优先级:最高 !>= <=== !=& &最低 | |同算术表达式一样,在关系或逻辑表达式中也使用括号来修改原计算顺序。切记,所有关系和逻辑表达式产生的结果不是 0就是1,所以下面的程序段不仅正确而且将在屏幕上打印数值 1。int x;x= 1 0 0 ;pr i n t f ( " % d " , x > 1 0 ) ;2.6.4 位操作符与其它语言不同, C语言支持全部的位操作符( Bitwise Operators)。因为C语言的设计目的是取代汇编语言,所以它必须支持汇编语言所具有的运算能力。位操作是对字节或字中的位(b i t)进行测试、置位或移位处理,这里字节或字是针对 C标准中的c h a r和i n t数据类型而言的。位操作不能用于 f l o a t、d o u b l e、long double、v o i d或其它复杂类型。表 2 - 7给出了位操作的操作符。位操作中的 A N D、O R和N O T(1的补码)的真值表与逻辑运算等价,唯一不同的是位操作是逐位进行运算的。表2-7 位操作符操 作 符 含 义 操 作 符 含 义& 与(A N D) ~ 1的补(N O T)| 或(O R) > > 右移^ 异或(X O R) < < 左移下面是异或的真值表。表2-8 异或的真值表P q p q0 0 01 0 11 1 00 1 1第2章 数据类型、运算符和表达式 2 5下载如表2 - 8所示,当且仅当一个操作数为 Tr u e时,异或的输出为Tr u e,否则为Fl a s e。位操作通常用于设备驱动程序,例如调制解调器程序、磁盘文件管理程序和打印机驱动程序。这是因为位操作可屏蔽掉某些位,如奇偶校验位(奇偶校验位用于确保字节中的其它位不会发生错误通常奇偶校验位是字节的最高位)。通常我们可把位操作 A N D作为关闭位的手段,这就是说两个操作数中任一为 0的位,其结果中对应位置为 0。例如,下面的函数通过调用函数 r e a d _ m o d e m ( ),从调制解调器端口读入一个字符,并将奇偶校验位置成 0。[例2 - 4 ]Char get_char_from_modem(){char ch;ch=read_modem(); /*从调制解调器端口中得到一个字符*/re t u r n ( c h & 1 2 7 ) ;}字节的位 8是奇偶位,将该字节与一个位 1到位7为1、位8为0的字节进行与操作,可将该字节的奇偶校验位置成 0。表达式 c h & 1 2 7正是将 c h中每一位同 1 2 7数字的对应位进行与操作,结果c h的位8被置成了0。在下面的例子中,假定 c h接收到字符 " A "并且奇偶位已经被置位。奇偶位↓110000001 内容为‘A’的c h,其中奇偶校验位为 10 11111111 二进制的1 2 7执行与操作& 与操作= 010000001 去掉奇偶校验的‘A’位操作O R与A N D操作相反,可用来置位。任一操作数中为 1的位将结果的对应位置 1。如下所示,1 2 8 | 3的情况是:1000000 128的二进制0 0 0 0 0 11 3的二进制| 或操作= 1 0 0 0 0 11 结果异或操作通常缩写为 X O R,当且仅当做比较的两位不同时,才将结果的对应位置位。如下所示,异或操作1 2 7 1 2 0的情况是:0 1111111 127 的二进制0 1111000 120的二进制^ 异或操作= 0 0 0 0 0 111 结果一般来说,位的A N D、O R和X O R操作通过对操作数运算,直接对结果变量的每一位分别处理。正是因为这一原因(还有其它一些原因),位操作通常不像关系和逻辑运算符那样用在条件语句中,我们可以用例子说明这一点:假定 X = 7,那么 x & & 8 为Tu r e ( 1 ) ,而x & 8却为Fl a s e ( 0 )。记住,关系和逻辑操作符结果不是 0就是1。而相似的位操作通过相应处理,结果可为任2 6 C语言程序设计下载意值。换言之,位操作可以有 0或1以外的其它值,而逻辑运算符的计算结果总是 0或1。移位操作符 > >和< <将变量的各位按要求向或向左移动。右移语句通常形式是:variable >>右移位数左移语句是:v a r i a b l e < <左移位数当某位从一端移出时,另一端移入 0(某些计算机是送 1,详细内容请查阅相应 C编译程序用户手册)。切记:移位不同于循环,从一端移出的位并不送回到另一端去,移去的位永远丢失了,同时在另一端补 0。移位操作可对外部设备(如 D / A转换器)的输入和状态信息进行译码,移位操作还可用于整数的快速乘除运算。如表 2 - 9所示(假定移位时补 0),左移一位等效于乘 2,而右移一位等效于除以2。表2-9 用移位操作进行乘和除字 符 x 每个语句执行后的 x x 的 值x = 7 0 0 0 0 0 111 7x < < 1 0 0 0 0 111 0 1 4x < < 3 0 111 0 0 0 0 11 2x < < 2 11 0 0 0 0 0 0 1 9 2x > > 1 0 11 0 0 0 0 0 9 6x > > 2 0 0 0 11 0 0 0 2 4每左移一位乘 2,注意 x < < 2后,原 x的信息已经丢失了,因为一位已经从一端出,每右移一位相当于被2除,注意,乘后再除时,除操作并不带回乘法时已经丢掉的高位。反码操作符为~。~的作用是将特定变量的各位状态取反,即将所有的 1位置成0,所有的 0位置成1。位操作符经常用在加密程序中,例如,若想生成一个不可读磁盘文件时,可以在文件上做一些位操作。最简单的方法是用下述方法,通过 1的反码运算,将每个字节的每一位取反。原字节 0 0 1 0 11 0 0第一次取反码 11 0 1 0 0 11第二次取反码 0 0 1 0 11 0 0注意,对同一行进行连续的两次求反,总是得到原来的数字,所以第一次求反表示了字节的编码,第二次求反进行译码又得到了原来的值。可以用下面的函数e n c o d e ( )对字符进行编码。[例2 - 5 ]char encode(ch)char ch;{return (~ch);}2.6.5 操作符C语言提供了一个可以代替某些 i f - t h e n - e l s e语句的简便易用的操作符?。该操作符是三元第2章 数据类型、运算符和表达式 2 7下载的,其一般形式为:EX P 1 E X E 2 : E X P 3E X P 1,E X P 2和E X P 3是表达式,注意冒号的用法和位置。操作符“ ”作用是这样的,在计算 E X P 1之后,如果数值为Tr u e,则计算E X P 2,并将结果作为整个表达式的数值;如果 E X P 1的值为 Fl a s e,则计算 E X P 3,并以它的结果作为整个表达式的值,请看下例:x= 1 0 ;y= x > 9 1 0 0 : 2 0 0 ;例中,赋给 y的数值是 1 0 0,如果x被赋给比9小的值,y的值将为2 0 0,若用i f - e l s e语句改写,有下面的等价程序:x= 1 0 ;if(x>9) y=100;else y=200;有关C语言中的其它条件语句将在第 3章进行讨论。2.6.6 逗号操作符作为一个操作符,逗号把几个表达式串在一起。逗号操作符的左侧总是作为 v o i d (无值),这意味着其右边表达式的值变为以逗号分开的整个表达式的值。例如:x= ( y = 3 , y + 1 ) ;这行将 3赋给y,然后将 4赋给x,因为逗号操作符的优先级比赋值操作符优先级低,所以必须使用括号。实际上,逗号表示操作顺序。当它在赋值语句右边使用时,所赋的值是逗号分隔开的表中最后那个表达式的值。例如,y= 1 0 ;x= ( y = y - 5 , 2 5 / y ) ;执行后,x的值是5,因为y的起始值是 1 0,减去5之后结果再除以2 5,得到最终结果。在某种意义上可以认为,逗号操作符和标准英语的 a n d是同义词。2.6.7 关于优先级的小结表2 - 1 0列出了C语言所有操作符的优先级,其中包括将在本书后面讨论的某些操作符。注意,所有操作符(除一元操作符和?之外)都是左结合的。一元操作符( *,&和-)及操作符“?”则为右结合。表2-10 C语言操作符的优先级最 高 级 ()[] →!~ ++ -- -(type) * & sizeof* / %+ -<< >><= >=== !=2 8 C语言程序设计下载(续)&^|& &| | = += -= *= /=最低级 ,2.7 表达式表达式由运算符、常量及变量构成。 C语言的表达式基本遵循一般代数规则,有几点却是与C语言紧密相关的,以下将分别加以讨论。2.7.1 表达式中的类型转换混合于同一表达式中的不同类型常量及变量,应均变换为同一类型的量。 C语言的编译程序将所有操作数变换为与最大类型操作数同类型。变换以一次一操作的方式进行。具体规则如下:char ch;int i;float f;double d;result=(ch / i) + ( f * d ) - ( f + i );int double doubleint double doubled o u b l ed o u b l e图2-1 类型转换实例1 ) 所有c h a r及short int 型量转为 i n t型,所有 f l o a t转换为d o u b l e。2) 如操作数对中一个为 long double ,另一个转换为 long double。① 要不然,一个为d o u b l e,另一个转为 d o u b l e。② 要不然,一个为 l o n g,另一个转为 l o n g。③ 要不然,一个为u n s i g n e d,另一个转为u n s i g n e d。一旦运用以上规则。每一对操作数均变为同类型。注意,规则 2 )有几种必须依次应用的条件。图2 - 1示出了类型转换。首先, char ch转换成 i n t,且float f 转换成d o u b l e;然后c h / i的结果转换成 d o u b l e,因为 f * d是d o u b l e;最后由于这次两个操作数都是 d o u b l e,所以结果也是第2章 数据类型、运算符和表达式 2 9下载d o u b l e .2.7.2 构成符cast可以通过称为c a s t的构成符强迫一表达式变为特定类型。其一般形式为:(ty p e )ex p r e s s i o n( t y p e )是标准 C语言中的一个数据类型。例如,为确保表达式 x / 2的结果具有类型 f l o a t,可写为:(fl o a t )x/ 2通常认为c a s t是操作符。作为操作符, c a s t是一元的,并且同其它一元操作符优先级相同。虽然c a s t在程序中用得不多,但有时它的使用的确很有价值。例如,假设希望用一整数控制循环,但在执行计算时又要有小数部分。[例2 - 6 ]main(){int i ;for (i+1;i<=100;++i)printf("%d/2 is :%f",i,(float)i/2);}若没有 c a s t ( f l o a t ),就仅执行一次整数除;有了 c a s t就可保证在屏幕上显示答案的小数部分。2.7.3 空格与括号为了增加可读性,可以随意在表达式中插入tab和空格符。例如,下面两个表达式是相同的。x= 1 0 / y * ( 1 2 7 / x ) ;x= 1 0 / y * ( 1 2 7 / x ) ;冗余的括号并不导致错误或减慢表达式的执行速度。我们鼓励使用括号,它可使执行顺序更清楚一些。例如,下面两个表达式中哪个更易读一些呢?x= y / 2 - 3 4 * t e m p & 1 2 7 ;x= ( y / 2 ) - ( ( 3 4 * t e m p ) & 1 2 7 ) ;2.7.4 C语言中的简写形式C语言提供了某些赋值语句的简写形式。例如语句:x= x + 1 0 ;在C语言中简写形式是:x+ = 1 0 ;这组操作符对 + =通知编译程序将 X + 1 0的值赋予 X。这一简写形式适于 C语言的所有二元操作符(需两个操作数的操作符)。在C语言中,variable=variable1 operator expression;3 0 C语言程序设计下载与variable1 operator=expression相同。请看另一个例子:x= x - 1 0 0 ;其等价语句是x- = 1 0 0 ;简写形式广泛应用于专业 C语言程序中,希望读者能熟悉它。(共145张PPT)10.1 概述10.2 插入排序10.3 快速排序10.4 堆排序10.5 归并排序10.6 基数排序10.7 各种排序方法的综合比较10.8 外部排序10.1 概 述一、排序的定义二、内部排序和外部排序三、内部排序方法的分类一、什么是排序? 排序是计算机内经常进行的一种操作,其目的是将一组“无序”的记录序列调整为“有序”的记录序列。例如:将下列关键字序列52, 49, 80, 36, 14, 58, 61, 23, 97, 75调整为14, 23, 36, 49, 52, 58, 61 ,75, 80, 97一般情况下,假设含n个记录的序列为{ R1, R2, …, Rn }其相应的关键字序列为 { K1, K2, …,Kn }这些关键字相互之间可以进行比较,即在它们之间存在着这样一个关系 Kp1≤Kp2≤…≤Kpn按此固有关系将上式记录序列重新排列为{ Rp1, Rp2, …,Rpn }的操作称作排序。二、内部排序和外部排序 若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序; 反之,若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序。三、内部排序的方法 内部排序的过程是一个逐步扩大记录的有序序列长度的过程。经过一趟排序有序序列区无 序 序 列 区有序序列区无 序 序 列 区 基于不同的“扩大” 有序序列长度的方法,内部排序方法大致可分下列几种类型:插入类交换类选择类归并类其它方法待排记录的数据类型定义如下:#define MAXSIZE 1000 // 待排顺序表最大长度typedef int KeyType; // 关键字类型为整数类型typedef struct {KeyType key; // 关键字项InfoType otherinfo; // 其它数据项} RcdType; // 记录类型typedef struct {RcdType r[MAXSIZE+1]; // r[0]闲置int length; // 顺序表长度} SqList; // 顺序表类型1. 插入类 将无序子序列中的一个或几个记录“插入”到有序序列中,从而增加记录的有序子序列的长度。2. 交换类 通过“交换”无序序列中的记录从而得到其中关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度。3. 选择类 从记录的无序子序列中“选择”关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度。4. 归并类 通过“归并”两个或两个以上的记录有序子序列,逐步增加记录有序序列的长度。5. 其它方法10. 2插 入 排 序有序序列R[1..i-1]R[i]无序序列 R[i..n]一趟直接插入排序的基本思想:有序序列R[1..i]无序序列 R[i+1..n]实现“一趟插入排序”可分三步进行:3.将R[i] 插入(复制)到R[j+1]的位置上。2.将R[j+1..i-1]中的所有记录均后移一个位置;1.在R[1..i-1]中查找R[i]的插入位置;R[1..j].key R[i].key < R[j+1..i-1].key直接插入排序(基于顺序查找)表插入排序(基于链表存储)不同的具体实现方法导致不同的算法描述折半插入排序(基于折半查找)希尔排序(基于逐趟缩小增量)一、直接插入排序 利用 “顺序查找”实现“在R[1..i-1]中查找R[i]的插入位置”算法的实现要点:从R[i-1]起向前进行顺序查找, 监视哨设置在R[0];R[0] = R[i]; // 设置“哨兵”循环结束表明R[i]的插入位置为 j +1R[0]jR[i]for (j=i-1; R[0].key// 从后往前找j=i-1插入位置对于在查找过程中找到的那些关键字不小于R[i].key的记录,并在查找的同时实现记录向后移动;for (j=i-1; R[0].keyR[j+1] = R[j]R[0]jR[i]j= i-1上述循环结束后可以直接进行“插入”插入位置令 i = 2,3,…, n,实现整个序列的排序。for ( i=2; i<=n; ++i )if (R[i].key{ 在 R[1..i-1]中查找R[i]的插入位置;插入R[i] ;}void InsertionSort ( SqList &L ) {// 对顺序表 L 作直接插入排序。for ( i=2; i<=L.length; ++i )if (L.r[i].key < L.r[i-1].key) {}} // InsertSortL.r[0] = L.r[i]; // 复制为监视哨for ( j=i-1; L.r[0].key < L.r[j].key; -- j )L.r[j+1] = L.r[j]; // 记录后移L.r[j+1] = L.r[0]; // 插入到正确位置内部排序的时间分析:实现内部排序的基本操作有两个:(2)“移动”记录。(1)“比较”序列中两个关键字的大小;对于直接插入排序:最好的情况(关键字在记录序列中顺序有序):“比较”的次数:最坏的情况(关键字在记录序列中逆序有序):“比较”的次数:0“移动”的次数:“移动”的次数:因为 R[1..i-1] 是一个按关键字有序的有序序列,则可以利用折半查找实现“在R[1..i-1]中查找R[i]的插入位置”,如此实现的插入排序为折半插入排序。二、折半插入排序void BiInsertionSort ( SqList &L ) {} // BInsertSort在 L.r[1..i-1]中折半查找插入位置;for ( i=2; i<=L.length; ++i ) {} // forL.r[0] = L.r[i]; // 将 L.r[i] 暂存到 L.r[0]for ( j=i-1; j>=high+1; --j )L.r[j+1] = L.r[j]; // 记录后移L.r[high+1] = L.r[0]; // 插入low = 1; high = i-1;while (low<=high) {}m = (low+high)/2; // 折半if (L.r[0].key < L.r[m].key)high = m-1; // 插入点在低半区else low = m+1; // 插入点在高半区ilowhighmmlowlowmhighilowhighmhighmhighmlow例如:再如:插入位置插入位置14 36 49 52 8058 61 23 97 75L.r14 36 49 52 58 61 8023 97 75L.r三、表插入排序为了减少在排序过程中进行的“移动”记录的操作,必须改变排序过程中采用的存储结构。利用静态链表进行排序,并在排序完成之后,一次性地调整各个记录相互之间的位置,即将每个记录都调整到它们所应该在的位置上。void LInsertionSort (Elem SL[ ], int n){// 对记录序列SL[1..n]作表插入排序。SL[0].key = MAXINT ;SL[0].next = 1; SL[1].next = 0;for ( i=2; i<=n; ++i )for ( j=0, k = SL[0].next;SL[k].key<=SL[i].key ; j=k, k=SL[k].next ){ SL[j].next = i; SL[i].next = k; }// 结点i插入在结点j和结点k之间}// LinsertionSort算法中使用了三个指针:其中:p指示第i个记录的当前位置;i指示第i个记录应在的位置;q指示第i+1个记录的当前位置如何在排序之后调整记录序列?void Arrange ( Elem SL[ ], int n ) {p = SL[0].next; // p指示第一个记录的当前位置for ( i=1; iwhile (pq = SL[p].next; // q指示尚未调整的表尾if ( p!= i ) {SL[p]←→SL[i]; // 交换记录,使第i个记录到位SL[i].next = p; // 指向被移走的记录, }//ifp = q; // p指示尚未调整的表尾,准备找第i+1个记录}//for} // Arrange四、希尔排序(又称缩小增量排序) 基本思想:对待排记录序列先作“宏观”调整,再作“微观”调整。所谓“宏观”调整,指的是,“跳跃式”的插入排序。具体做法为:将记录序列分成若干子序列,分别对每个子序列进行插入排序。其中,d 称为增量,它的值在排序过程中从大到小逐渐缩小,直至最后一趟排序减为 1。例如:将 n 个记录分成 d 个子序列:{ R[1],R[1+d],R[1+2d],…,R[1+kd] }{ R[2],R[2+d],R[2+2d],…,R[2+kd] }…{ R[d],R[2d],R[3d],…,R[kd],R[(k+1)d] }例如:16 25 12 30 47 11 23 36 9 18 31第一趟希尔排序,设增量 d =511 23 12 9 18 16 25 36 30 47 31第二趟希尔排序,设增量 d = 39 18 12 11 23 16 25 31 30 47 36第三趟希尔排序,设增量 d = 19 11 12 16 18 23 25 30 31 36 47void ShellInsert ( SqList &L, int dk ) {for ( i=dk+1; i<=n; ++i )if ( L.r[i].key< L.r[i-dk].key) {L.r[0] = L.r[i]; // 暂存在L.r[0]for (j=i-dk; j>0&&(L.r[0].keyj-=dk)L.r[j+dk] = L.r[j]; // 记录后移,查找插入位置L.r[j+dk] = L.r[0]; // 插入} // if} // ShellInsertvoid ShellSort (SqList &L, int dlta[], int t){ // 增量为dlta[]的希尔排序for (k=0; kShellInsert(L, dlta[k]);//一趟增量为dlta[k]的插入排序} // ShellSort10.3 快 速 排 序一、起泡排序二、一趟快速排序三、快速排序四、快速排序的时间分析一、起泡排序 假设在排序过程中,记录序列R[1..n]的状态为:第 i 趟起泡排序无序序列R[1..n-i+1]有序序列 R[n-i+2..n]n-i+1无序序列R[1..n-i]有序序列 R[n-i+1..n]比较相邻记录,将关键字最大的记录交换到 n-i+1 的位置上void BubbleSort(Elem R[ ], int n) {while (i >1) {} // while} // BubbleSorti = n;i = lastExchangeIndex; // 本趟进行过交换的// 最后一个记录的位置if (R[j+1].key < R[j].key) {Swap(R[j], R[j+1]);lastExchangeIndex = j; //记下进行交换的记录位置} //iffor (j = 1; j < i; j++)lastExchangeIndex = 1;注意:2. 一般情况下,每经过一趟“起泡”,“i 减一”,但并不是每趟都如此。例如:2553157989i=7i=6for (j = 1; j < i; j++) if (R[j+1].key < R[j].key) …13i=21. 起泡排序的结束条件为,最后一趟没有进行“交换记录”。时间分析:最好的情况(关键字在记录序列中顺序有序):只需进行一趟起泡“比较”的次数:最坏的情况(关键字在记录序列中逆序有序):需进行n-1趟起泡“比较”的次数:0“移动”的次数:“移动”的次数:n-1二、一趟快速排序(一次划分) 目标:找一个记录,以它的关键字作为“枢轴”,凡其关键字小于枢轴的记录均移动至该记录之前,反之,凡关键字大于枢轴的记录均移动至该记录之后。致使一趟排序之后,记录的无序序列R[s..t]将分割成两部分:R[s..i-1]和R[i+1..t], 且R[j].key≤ R[i].key ≤ R[j].key(s≤j≤i-1) 枢轴 (i+1≤j≤t)stlowhigh设 R[s]=52 为枢轴暂存在R[0]的位置上将 R[high].key 和 枢轴的关键字进行比较,要求R[high].key ≥ 枢轴的关键字将 R[low].key 和 枢轴的关键字进行比较,要求R[low].key ≤ 枢轴的关键字high23low80high14low52例如R[0]52lowhighhighhighlow可见,经过“一次划分” ,将关键字序列52, 49, 80, 36, 14, 58, 61, 97, 23, 75调整为: 23, 49, 14, 36, (52) 58, 61, 97, 80, 75在调整过程中,设立了两个指针: low 和high,它们的初值分别为: s 和 t,之后逐渐减小 high,增加 low,并保证R[high].key≥52,和 R[low].key≤52,否则进行记录的“交换”。int Partition (RedType& R[], int low, int high) {pivotkey = R[low].key;while (lowwhile (low=pivotkey)--high;R[low]←→R[high];while (low++low;R[low]←→R[high];}return low; // 返回枢轴所在位置} // Partitionint Partition (RedType R[], int low, int high) {}// PartitionR[0] = R[low]; pivotkey = R[low].key; // 枢轴while (low}while(low=pivotkey)-- high; // 从右向左搜索R[low] = R[high];while (low++ low; // 从左向右搜索R[high] = R[low];R[low] = R[0]; return low;三、快速排序首先对无序的记录序列进行“一次划分”,之后分别对分割所得两个子序列“递归”进行快速排序。无 序 的 记 录 序 列无序记录子序列(1)无序子序列(2)枢轴一次划分分别进行快速排序void QSort (RedType & R[], int s, int t ) {// 对记录序列R[s..t]进行快速排序if (s < t-1) { // 长度大于1}} // QSortpivotloc = Partition(R, s, t);// 对 R[s..t] 进行一次划分QSort(R, s, pivotloc-1);// 对低子序列递归排序,pivotloc是枢轴位置QSort(R, pivotloc+1, t); // 对高子序列递归排序void QuickSort( SqList & L) {// 对顺序表进行快速排序QSort(L.r, 1, L.length);} // QuickSort第一次调用函数 Qsort 时,待排序记录序列的上、下界分别为 1 和 L.length。四、快速排序的时间分析 假设一次划分所得枢轴位置 i=k,则对n 个记录进行快排所需时间 其中 Tpass(n)为对 n 个记录进行一次划分所需时间,若待排序列中记录的关键字是随机分布的,则 k 取 1 至 n 中任意一值的可能性相同。T(n) = Tpass(n) + T(k-1) + T(n-k)设 Tavg(1)≤b则可得结果:结论: 快速排序的时间复杂度为O(nlogn)由此可得快速排序所需时间的平均值为:若待排记录的初始状态为按关键字有序时,快速排序将蜕化为起泡排序,其时间复杂度为O(n2)。为避免出现这种情况,需在进行一次划分之前,进行“予处理”,即:先对 R(s).key, R(t).key 和 R[ (s+t)/2 .key,进行相互比较,然后取关键字为“三者之中”的记录为枢轴记录。10.4 堆 排 序简 单 选 择 排 序堆 排 序一、简单选择排序假设排序过程中,待排记录序列的状态为:有序序列R[1..i-1]无序序列 R[i..n]第 i 趟简单选择排序从中选出关键字最小的记录有序序列R[1..i]无序序列 R[i+1..n]简单选择排序的算法描述如下:void SelectSort (Elem R[], int n ) {// 对记录序列R[1..n]作简单选择排序。for (i=1; i// 选择第 i 小的记录,并交换到位}} // SelectSortj = SelectMinKey(R, i);// 在 R[i..n] 中选择关键字最小的记录if (i!=j) R[i]←→R[j];// 与第 i 个记录交换时间性能分析 对 n 个记录进行简单选择排序,所需进行的 关键字间的比较次数 总计为 移动记录的次数,最小值为 0, 最大值为3(n-1)二、堆排序堆是满足下列性质的数列{r1, r2, …,rn}:或堆的定义:{12, 36, 27, 65, 40, 34, 98, 81, 73, 55, 49}例如:是小顶堆{12, 36, 27, 65, 40, 14, 98, 81, 73, 55, 49}不是堆(小顶堆)(大顶堆)rir2ir2i+1若将该数列视作完全二叉树,则 r2i 是 ri 的左孩子; r2i+1 是 ri 的右孩子。1236276549817355403498例如:是堆14不 堆排序即是利用堆的特性对记录序列进行排序的一种排序方法。例如:建大顶堆{ 98, 81, 49, 73, 36, 27, 40, 55, 64, 12 }{ 12, 81, 49, 73, 36, 27, 40, 55, 64, 98 }交换 98 和 12重新调整为大顶堆{ 81, 73, 49, 64, 36, 27, 40, 55, 12, 98 }{ 40, 55, 49, 73, 12, 27, 98, 81, 64, 36 }经过筛选void HeapSort ( HeapType &H ) {// 对顺序表 H 进行堆排序。} // HeapSortfor ( i=H.length/2; i>0; --i )HeapAdjust ( H.r, i, H.length ); // 建大顶堆for ( i=H.length; i>1; --i ) {H.r[1]←→H.r[i];// 将堆顶记录和当前未经排序子序列// H.r[1..i]中最后一个记录相互交换HeapAdjust(H.r, 1, i-1); // 对 H.r[1] 进行筛选}如何“建堆”?两个问题:如何“筛选”?定义堆类型为:typedef SqList HeapType;// 堆采用顺序表表示之所谓“筛选”指的是,对一棵左/右子树均为堆的完全二叉树,“调整”根结点使整个二叉树也成为一个堆。堆堆筛选98814973556412362740例如:是大顶堆12但在 98 和 12 进行互换之后,它就不是堆了因此,需要对它进行“筛选”98128173641298比较比较void HeapAdjust (RcdType &R[], int s, int m){ // 已知 R[s..m]中记录的关键字除 R[s] 之外均// 满足堆的特征,本函数自上而下调整 R[s] 的// 关键字,使 R[s..m] 也成为一个大顶堆。} // HeapAdjustrc = R[s]; // 暂存 R[s]for ( j=2*s; j<=m; j*=2 ) { // j 初值指向左孩子自上而下的筛选过程;}R[s] = rc; // 将调整前的堆顶记录插入到 s 位置if ( rc.key >= R[j].key ) break;// 再作“根”和“子树根”之间的比较,// 若“>=”成立,则说明已找到 rc 的插// 入位置 s ,不需要继续往下调整R[s] = R[j]; s = j;// 否则记录上移,尚需继续往下调整if ( j// 左/右“子树根”之间先进行相互比较// 令 j 指示关键字较大记录的位置建堆是一个从下往上进行“筛选”的过程。40554973816436122798例如: 排序之前的关键字序列为123681734998817355现在,左/右子树都已经调整为堆,最后只要调整根结点,使整个二叉树是个“堆”即可。98494064361227堆排序的时间复杂度分析:1. 对深度为 k 的堆,“筛选”所需进行的关键字比较的次数至多为2(k-1);2. 对 n 个关键字,建成深度为 h (= log2n +1) 的堆,所需进行的关键字比较的次数至多 4n;3. 调整“堆顶” n-1 次,总共进行的关键字比较的次数不超过2 ( log2(n-1) + log2(n-2) + …+log22) < 2n( log2n )因此,堆排序的时间复杂度为O(nlogn)10.5 归 并 排 序 归并排序的过程基于下列基本思想进行:将两个或两个以上的有序子序列 “归并” 为一个有序序列。 在内部排序中,通常采用的是2-路归并排序。即:将两个位置相邻的记录有序子序列归并为一个记录的有序序列。有 序 序 列 R[l..n]有序子序列 R[l..m]有序子序列 R[m+1..n]这个操作对顺序表而言,是轻而易举的。void Merge (RcdType SR[], RcdType &TR[],int i, int m, int n) {// 将有序的记录序列 SR[i..m] 和 SR[m+1..n]// 归并为有序的记录序列 TR[i..n]} // Mergefor (j=m+1, k=i; i<=m && j<=n; ++k){ // 将SR中记录由小到大地并入TRif (SR[i].key<=SR[j].key) TR[k] = SR[i++];else TR[k] = SR[j++];}… …if (i<=m) TR[k..n] = SR[i..m];// 将剩余的 SR[i..m] 复制到 TRif (j<=n) TR[k..n] = SR[j..n];// 将剩余的 SR[j..n] 复制到 TR归并排序的算法 如果记录无序序列 R[s..t] 的两部分R[s.. (s+t)/2 ] 和 R[ (s+t)/2 +1..t]分别按关键字有序,则利用上述归并算法很容易将它们归并成整个记录序列是一个有序序列。由此,应该先分别对这两部分进行 2-路归并排序。例如:52, 23, 80, 36, 68, 14 (s=1, t=6)[ 52, 23, 80] [36, 68, 14][ 52, 23][80][ 52][ 23, 52][ 23, 52, 80][36, 68][14][36][68][36, 68][14, 36, 68][ 14, 23, 36, 52, 68, 80 ][23]void Msort ( RcdType SR[],RcdType &TR1[], int s, int t ) {// 将SR[s..t] 归并排序为 TR1[s..t]if (s= =t) TR1[s]=SR[s];else{}} // Msort… …m = (s+t)/2;// 将SR[s..t]平分为SR[s..m]和SR[m+1..t]Msort (SR, TR2, s, m);// 递归地将SR[s..m]归并为有序的TR2[s..m]Msort (SR, TR2, m+1, t);//递归地SR[m+1..t]归并为有序的TR2[m+1..t]Merge (TR2, TR1, s, m, t);// 将TR2[s..m]和TR2[m+1..t]归并到TR1[s..t]void MergeSort (SqList &L) {// 对顺序表 L 作2-路归并排序。MSort(L.r, L.r, 1, L.length);} // MergeSort 容易看出,对 n 个记录进行归并排序的时间复杂度为Ο(nlogn)。即:每一趟归并的时间复杂度为 O(n),总共需进行 log2n 趟。10.6 基 数 排 序 基数排序是一种借助“多关键字排序”的思想来实现“单关键字排序”的内部排序算法。多关键字的排序链式基数排序一、多关键字的排序n 个记录的序列 { R1, R2, …,Rn}对关键字 (Ki0, Ki1,…,Kid-1) 有序是指:其中: K0 被称为 “最主”位关键字,Kd-1 被称为 “最次”位关键字。对于序列中任意两个记录 Ri 和 Rj(1≤i(Ki0, Ki1, …,Kid-1) < (Kj0, Kj1, …,Kjd-1)实现多关键字排序通常有两种作法:最低位优先LSD法:最高位优先MSD法:先对K0进行排序,并按 K0 的不同值将记录序列分成若干子序列之后,分别对 K1 进行排序,...…, 依次类推,直至最后对最次位关键字排序完成为止。先对 Kd-1 进行排序,然后对 Kd-2 进行排序,依次类推,直至对最主位关键字 K0 排序完成为止。排序过程中不需要根据 “前一个” 关键字的排序结果,将记录序列分割成若干个(“前一个”关键字不同的)子序列。 例如:学生记录含三个关键字:系别、班号和班内的序列号,其中以系别为最主位关键字。无序序列对K2排序对K1排序对K0排序3,2,301,2,153,1,202,3,182,1,201,2,152,3,183,1,202,1,203,2,303,1,202,1,201,2,153,2,302,3,181,2,152,1,202,3,183,1,203,2,30LSD的排序过程如下:二、链式基数排序 假如多关键字的记录序列中,每个关键字的取值范围相同,则按LSD法进行排序时,可以采用“分配-收集”的方法,其好处是不需要进行关键字间的比较。 对于数字型或字符型的单关键字,可以看成是由多个数位或多个字符构成的多关键字,此时可以采用这种“分配-收集”的办法进行排序,称作基数排序法。例如:对下列这组关键字{209, 386, 768, 185, 247, 606, 230, 834, 539 }首先按其 “个位数” 取值分别为 0, 1, …, 9 “分配” 成 10 组,之后按从 0 至 9 的顺序将 它们 “收集” 在一起;然后按其 “十位数” 取值分别为 0, 1, …, 9 “分配” 成 10 组,之后再按从 0 至 9 的顺序将它们 “收集” 在一起;最后按其“百位数”重复一遍上述操作。 在计算机上实现基数排序时,为减少所需辅助存储空间,应采用链表作存储结构,即链式基数排序,具体作法为:1.待排序记录以指针相链,构成一个链表;2.“分配” 时,按当前“关键字位”所取值,将记录分配到不同的 “链队列” 中,每个队列中记录的 “关键字位” 相同;3.“收集”时,按当前关键字位取值从小到大将各队列首尾相链成一个链表;4.对每个关键字位均重复 2) 和 3) 两步。例如:p→369→367→167→239→237→138→230→139进行第一次分配进行第一次收集f[0] r[0]f[7] r[7]f[8] r[8]f[9] r[9]p→230→230←→367 ←→167→237→367→167→237→138→368→239→139→369 ←→239→139→138←进行第二次分配p→230→237→138→239→139p→230→367→167→237→138→368→239→139f[3] r[3]f[6] r[6]→230 ←→237→138→239→139→367 ←→167→368→367→167→368进行第二次收集进行第三次收集之后便得到记录的有序序列f[1] r[1]p→230→237→138→239→139→367→167→368进行第三次分配f[2] r[2]f[3] r[3]→138 ←→139→167→230 ←→237→239→367 ←→368p→138→139→167→230→237→239→367→368提醒注意:1.“分配”和“收集”的实际操作仅为修改链表中的指针和设置队列的头、尾指针;2.为查找使用,该链表尚需应用算法Arrange 将它调整为有序表。基数排序的时间复杂度为O(d(n+rd))其中,分配为O(n);收集为O(rd)(rd为“基”),d为“分配-收集”的趟数。10.7各种排序方法的综合比较一、时间性能1. 平均的时间性能基数排序时间复杂度为 O(nlogn):快速排序、堆排序和归并排序时间复杂度为 O(n2):直接插入排序、起泡排序和简单选择排序时间复杂度为 O(n):2. 当待排记录序列按关键字顺序有序时3. 简单选择排序、堆排序和归并排序的时间性能不随记录序列中关键字的分布而改变。直接插入排序和起泡排序能达到O(n)的时间复杂度;快速排序的时间性能蜕化为O(n2)二、空间性能指的是排序过程中所需的辅助空间大小1. 所有的简单排序方法(包括:直接插入、起泡和简单选择) 和堆排序的空间复杂度为O(1);2. 快速排序为O(logn),为递归程序执行过程中,栈所需的辅助空间;3. 归并排序所需辅助空间最多,其空间复杂度为 O(n);4. 链式基数排序需附设队列首尾指针,则空间复杂度为 O(rd)。三、排序方法的稳定性能1. 稳定的排序方法指的是,对于两个关键字相等的记录,它们在序列中的相对位置,在排序之前和经过排序之后,没有改变。2. 当对多关键字的记录序列进行LSD方法排序时,必须采用稳定的排序方法。排序之前 : { · · · · · Ri(K) · · · · · Rj(K) · · · · · }排序之后 : { · · · · · Ri(K) Rj(K) · · · · ·· · · · · }例如:排序前 ( 56, 34, 47, 23, 66, 18, 82, 47 )若排序后得到结果( 18, 23, 34, 47, 47, 56, 66, 82 )则称该排序方法是稳定的;若排序后得到结果( 18, 23, 34, 47, 47, 56, 66, 82 )则称该排序方法是不稳定的;3. 对于不稳定的排序方法,只要能举出一个实例说明即可。4. 快速排序、堆排序和希尔排序是不稳定的排序方法。例如 : 对 { 4, 3, 4, 2 } 进行快速排序,得到 { 2, 3, 4, 4 }四、关于“排序方法的时间复杂度的下限”本章讨论的各种排序方法,除基数排序外,其它方法都是基于“比较关键字”进行排序的排序方法。可以证明, 这类排序法可能达到的最快的时间复杂度为O(nlogn)。 (基数排序不是基于“比较关键字”的排序方法,所以它不受这个限制)例如:对三个关键字进行排序的判定树如下:K1K1K1K2K2< K3K2K1K3K2K3K11.树上的每一次“比较”都是必要的;2.树上的叶子结点包含所有可能情况。一般情况下,对n个关键字进行排序,可能得到的结果有n! 种,由于含n! 个叶子结点的二叉树的深度不小于 log2(n!) +1, 则对 n 个关键字进行排序的比较次数至少是 log2(n!) nlog2n (斯蒂林近似公式)。所以,基于“比较关键字”进行排序的排序方法,可能达到的最快的时间复杂度为 O(nlogn)。习题与练习一、基本知识题1. 解释下列概念(1) 排序(2) 内部排序(3) 堆(4) 稳定排序2. 回答下面问题(1) 5000个无序的数据,希望用最快速度挑选出其中前10个最大的元素,在快速排序、堆排序、归并排序和基数排序中采用哪种方法最好?为什么?(2) 大多数排序算法都有哪两个基本操作?3. 已知序列{17,25,55,43,3,32,78,67,91},请给出采用冒泡排序法对该序列作递增排序时每一趟的结果。4. 已知序列{491,77,572,16,996,101,863,258,689,325},请分别给出采用快速排序、堆排序和基数排序法对该序列作递增排序时每一趟的结果。5. 已知序列{86,94,138,62,41,54,18,32},请给出采用插入排序法对该序列作递增排序时,每一趟的结果。6. 已知序列{27,35,11,9,18,30,3,23,35,20},请给出采用归并排序法对该序列作递增排序时的每一趟的结果。二、算法设计题1. 一个线性表中的元素为全部为正或者负整数,试设计一算法,在尽可能少的时间内重排该表,将正、负整数分开,使线性表中所有负整数在正整数前面。2. 设计一个用单链表作存储结构的直接插入排序算法。3. 试设计一个算法实现双向冒泡排序。(双向冒泡排序就是在排序的过程中交替改变扫描方向。)4. 设一个一维数组A[n]中存储了n个互不相同的整数,且这些整数的值都在0到n-1之间,即A中存储了从0到n-1这n个整数。试编写一算法将A排序,结果存放在数组B[n]中,要求算法的时间复杂性为O(n)。返回习题解答一、基本知识题答案1. 答:(1) 排序:将一组杂乱无序的数据按一定的规律顺次排列起来叫做排序。(2) 内部排序:数据存储在内存中,并在内存中加以处理的排序方法叫内部排序。(3) 堆:堆是一个完全二叉树,它的每个结点对应于原始数据的一个元素,且规定如果一个结点有儿子结点,此结点数据必须大于或等于其儿子结点数据。(4) 稳定排序:一种排序方法,若排序后具有相同关键字的记录仍维持原来的相对次序,则称之为稳定的,否则称为不稳定的。2. 答:(1)采用堆排序最好。因为以上几种算法中,快速排序、归并排序和基数排序都是在排序结束后才能确定数据元素的全部顺序,而无法知道排序过程中部分元素的有序性。堆排序则每次输出一个最大(或最小)的元素,然后对堆进行调整,保证堆顶的元素总是余下元素中最大(或最小)的。根据题意,只要选取前10个最大的元素,故采用堆排序方法是合适的。(2)两个基本操作:比较两个关键字的大小、改变指向记录的指针或移动记录本身。3. 答:采用冒泡排序法排序时的各趟结果如下:初始:17,25,55,43,3,32,78,67,91第1趟:17,25,43,3,32,55,67,78,91第2趟:17,25,3,32,43,55,67,78,91第3趟:17,3,25,32,43,55,67,78,91第4趟:3,17,25,32,43,55,67,78,91第5趟:3,17,25,32,43,55,67,78,91第5趟无元素交换,排序结束。4. 答:采用快速排序法排序时的各趟结果如下:初始:491,77,572,16,996,101,863,258,689,325第1趟:[325,77,258,16,101] 491 [863,996,689,572]第2趟:[101,77,258,16] 325,491 [863,996,689,572]第3趟:[16,77] 101 [258] 325,491 [863,996,689,572]第4趟:16 [77] 101 [258] 325,491 [863,996,689,572]第5趟:16,77,101 [258] 325,491 [863,996,689,572]第6趟:16,77,101,258,325,491 [863,996,689,572]第7趟:16,77,101,258,325,491 [572,689] 863 [996]第8趟:16,77,101,258,325,491,572 [689] 863 [996]第9趟:16,77,101,258,325,491,572,689,863 [996]第10趟:16,77,101,258,325,491,572,689,863,996采用堆排序法排序时各趟的结果如下图所示:(a) 初始堆(b) 建堆(c) 交换996和77,输出996(d) 筛选调整(e) 交换863和16,输出863(f) 筛选调整(g) 交换689和16,输出689(h) 筛选调整(i) 交换572和77,输出572(j) 筛选调整(k) 交换491和16,输出491(l) 筛选调整(m) 交换325和77,输出325(n) 筛选调整(o) 交换258和16,输出258(p) 筛选调整(q) 交换101和16,输出101(r) 筛选调整(s) 交换77和16,输出77(t) 输出16采用基数排序法排序时各趟的结果如下:初始:491,77,572,16,996,101,863,258,689,325第1趟(按个位排序):491,101,572,863,352,16,996,77,258,689第2趟(按十位排序):101,16,352,258,863,572,77,689,491,996第3趟(按百位排序):16,77,101,258,352,491,572,689,863,9965. 答:采用插入排序法排序时各趟的结果如下:初始:(86),94,138,62,41,54,18,32第1趟:(86,94),138,62,41,54,18,32第2趟:(86,94,138),62,41,54,18,32第3趟:(62,86,94,138),41,54,18,32第4趟:(41,62,86,94,138),54,18,32第5趟:(41,54,62,86,94,138),18,32第6趟:(18,41,54,62,86,94,138),32第7趟:(18,32,41,54,62,86,94,138)6. 答:采用归并排序法排序时各趟的结果如下:初始:27,35,11,9,18,30,3,23,35,20第1趟:[27,35] [9,11] [18,30] [3,23] [20,35]第2趟:[9,11,27,35] [3,18,23,30] [20,35]第3趟:[9,11,27,35] [3,18,20,23,30,35]第4趟:[3,9,11,18,20,23,27,30,35,35]二、算法设计题答案1. 解:本题的算法思想是:设置两个变量分别指向表的首尾,它们分别向中间移动,指向表首的如果遇到正整数,指向表尾的如果遇到负整数则互相交换,然后继续移动直至两者相遇。实现本题功能的算法如下:void part(int array[],int n){int i,j;i=1;j=n;while(i{ while(ii++;while(i=0)j--;if(i{ array[0]=array[i];array[i]=array[j];array[j]=array[0];}}}2. 解: 实现本题功能的算法如下:void insertsort(node *head){node *p,*q,*pre;pre=head;p=head->next; /*p指向待插入的元素*/while(p!=NULL){q=head;if(p->keykey) /*插到表首*/{ pre->next =p->next;p->next =head;head=p;}else{while(q->next!=p && q->next ->keykey)q=q->next;if(q->next ==p){pre=p;p=p->next;}else{ pre->next =p->next;p->next =q->next;q->next =p;p=pre->next;}}}}3. 解:实现本题功能的算法如下:void dbubblesort(sqlist r,int n){int i,j,flag;flag=1;i=1;while(flag!=0){flag=0;for(j=i;j{if(r[j]>r[j+1]){flag=1;r[0]=r[j];r[j]=r[j+1];r[j+1]=r[0];}}for(j=n-i;j>i;j--){if(r[j]{ flag=1;r[0]=r[j];r[j]=r[j-1];r[j-1]=r[0];}}i++;}}4. 解:实现本题功能的算法如下:void sort(int A[n],int B[n]){int i;for(i=0;iB[A[i]]=A[i];}返回根据给定的含有13个元素的有序表构造二叉排序树,在其中查找元素成功时平均查找长度为多少?顺序表和链表中结点的访问特性分别是什么?(A. 顺序访问;B. 随机访问)3叉树中度为2,3的结点个数为2,3,则其中叶子结点的个数为 。若程序中用以下语句说明循环队列queue:ElemType queue[MaxSize];int front=rear=-1;/*队首和队尾指针分别为front、rear;*/请问队列为空和队列为满分别满足什么条件?5. 一个非连通图有n个顶点和n-1条边,则此图中一定存在有什么特征?6. 元素之间的关系在顺序存储结构中通过什么来体现(或映射)?7. 下面语句段中有@标记的语句的执行频度是多少?int x=91,y=100;while(y>0)@ if(x>100) {x- =10;y--;}else x++;按照四则运算加、减、乘、除和幂运算(^)优先关系的惯例,给出对下列算术表达式求值时操作数栈和运算符栈的变化过程: A-B*C/D^F已知二叉树的中序和后序遍历序列分别为DGBAECF和GDBEFCA,请给出二叉树示意图并给出先序遍历序列、顺序存储结构示意图。已知某有序顺序表含有13个数据元素,根据给定条件在该表中进行二分查找。请给出二分查找判定树并计算查找成功时平均查找长度。给出对如图所示网络进行深度优先遍历和宽度优先遍历时输出的结点序列,并给出利用Prim算法构造其最小生成树过程及其用到的两个向量的变化过程。有无序数据序列38,49,21,97,38,49,15,48,7,56, 请回答以下问题:1.对此序列进行冒泡排序第二趟排序结束的数据序列;2.对此序列进行快速排序第一趟排序结束的数据序列;3.对此序列进行堆排序第一趟排序结束的数据序列;4. 对此序列进行按增量5,3希尔排序二趟排序后的数据序列。13、根据给定的序列{25,18,46,2,53,39,32,4,74,67}构造平衡二叉树。14、根据给定的权值集合W={0.05, 0.29, 0.07, 0.08, 0.14, 0.23, 0.03, 0.11}构造哈夫曼树和编码。15、算法设计题:1.将给出非递归的后序遍历算法。2.请设计算法:将给定的带头结点的单链表中的结点就地倒置。14′要求:利用原有的链表结点空间,不额外申请链表的结点空间。第2章 数据类型、运算符和表达式2.1 C语言的数据类型下载C语言有五种基本数据类型:字符、整型、单精度实型、双精度实型和空类型。尽管这几种类型数据的长度和范围随处理器的类型和 C语言编译程序的实现而异,但以 bit 为例,整数与CPU字长相等,一个字符通常为一个字节,浮点值的确切格式则根据实现而定。对于多数微机,表2-1给出了五种数据的长度和范围。表2-1 基本类型的字长和范围类 型char(字符型)int(整型)float(单精度型)double(双精度型)void(空值型)长 度(bit)81632640范 围0~255-32768~32767约精确到 6位数约精确到 12位数无值表中的长度和范围的取值是假定 CPU的字长为 16bit。C语言还提供了几种聚合类型( aggregate types),包括数组、指针、结构、共用体(联合)、位域和枚举。这些复杂类型在以后的章节中讨论。除void类型外,基本类型的前面可以有各种修饰符。修饰符用来改变基本类型的意义,以便更准确地适应各种情况的需求。修饰符如下: signed(有符号)。 unsigned(无符号)。 long(长型符)。 short(短型符)。修饰符 signed、short、long和unsigned适用于字符和整数两种基本类型,而 long还可用于double(注意,由于long float与double意思相同,所以 ANSI标准删除了多余的 long float)。表2-2给出所有根据 ANSI标准而组合的类型、字宽和范围。切记,在计算机字长大于 16位的系统中, short int与signed char可能不等。表2-2 ANSI标准中的数据类型类 型char(字符型)unsigned char(无符号字符型)signed char(有符号字符型 )int(整型)unsigned int(无符号整型 )signed int(有符号整型 )长 度(bit)888161616范 围ASCII字符0~255-128~12732768~327670~65535同int下载第2章 数据类型、运算符和表达式15(续)类 型short int(短整型)unsigned short int(无符号短整型)signed short int(有符号短整型 )long int(长整型)signed long int(有符号长整型 )unsigned long int(无符号长整型)float(单精度型)double(双精度型)长 度(bit)8883232323264范 围128~1270~255同shortint2147483648~21474836492147483648~21474836490~4294967296约精确到6位数约精确到12位数*表中的长度和范围的取值是假定 CPU的字长为16bit。因为整数的缺省定义是有符号数,所以 singed这一用法是多余的,但仍允许使用。某些实现允许将 unsigned用于浮点型,如 unsigned double。但这一用法降低了程序的可移植性,故建议一般不要采用。为了使用方便,C编译程序允许使用整型的简写形式: short int long int简写为short。简写为long。 unsigned short int 简写为unsigned short。 unsigned int简写为unsigned。 unsigned long int 简写为unsigned long。即,int可缺省。2.2 常量与变量2.2.1 标识符命名在C语言中,标识符是对变量、函数标号和其它各种用户定义对象的命名。标识符的长度可以是一个或多个字符。绝大多数情况下,标识符的第一个字符必须是字母或下划线,随后的字符必须是字母、数字或下划线(某些 C语言编译器可能不允许下划线作为标识符的起始字符)。下面是一些正确或错误标识符命名的实例。正确形式counttest23high_balance错误形式2counthi! therehigh..balanceANSI标准规定,标识符可以为任意长度,但外部名必须至少能由前 8个字符唯一地区分。这里外部名指的是在链接过程中所涉及的标识符,其中包括文件间共享的函数名和全局变量名。这是因为对某些仅能识别前 8个字符的编译程序而言,下面的外部名将被当作同一个标识符处理。counterscounters1counters2ANSI标准还规定内部名必须至少能由前 31个字符唯一地区分。内部名指的是仅出现于定16C语言程序设计义该标识符的文件中的那些标识符。下载C语言中的字母是有大小写区别的,因此 count Count COUNT是三个不同的标识符。标识符不能和C语言的关键字相同,也不能和用户已编制的函数或 C语言库函数同名。2.2.2 常量C语言中的常量是不接受程序修改的固定值,常量可为任意数据类型,如下例所示 :数据类型charintlong intshort intunsigned intfloatdouble常量举例'a'、'\n'、'9'21、 123 、2100 、-23435000、 -3410、-12、9010000、 987、 40000123.23、 4.34e-3123.23、 12312333、 -0.9876234C语言还支持另一种预定义数据类型的常量,这就是串。所有串常量括在双撇号之间,例如"This is a test"。切记,不要把字符和串相混淆,单个字符常量是由单撇号括起来的,如 'a'。2.2.3 变量其值可以改变的量称为变量。一个变量应该有一个名字 (标识符 ),在内存中占据一定的存储单元,在该存储单元中存放变量的值。请注意区分变量名和变量值这两个不同的概念。所有的C变量必须在使用之前定义。定义变量的一般形式是:type variable_list;这里的type必须是有效的C数据类型,variable_list(变量表)可以由一个或多个由逗号分隔的多个标识符名构成。下面给出一些定义的范例。int i, j, l;short int si;unsigned int ui;double balance, profit,loss;注意 C语言中变量名与其类型无关。2.3 整型数据2.3.1 整型常量整型常量及整常数。它可以是十进制、八进制、十六进制数字表示的整数值。十进制常数的形式是:digits这里digits可以是从 0到9的一个或多个十进制数位,第一位不能是 0。八进制常数的形式是:下载0digits第2章 数据类型、运算符和表达式17在此,digits可以是一个或多个八进制数( 0~7之间),起始0是必须的引导符。十六进制常数是下述形式:0xhdigits0Xhdigits这里hdigits可以是一个或多个十六进制数(从 0~9的数字,并从“ a”~“ f”的字母)。引导符0是必须有的,X即字母可用大写或小写。注意,空白字符不可出现在整数数字之间。表 2-3列出了整常数的形式。表2-3 整常数的例子十 进 制1013232179八 进 制0120204076663十 六 进 制0Xa或0XA0X840X7db3或0X7DB3整常数在不加特别说明时总是正值。如果需要的是负值,则负号“ -”必须放置于常数表达式的前面。每个常数依其值要给出一种类型。当整常数应用于一表达式时,或出现有负号时,常数类型自动执行相应的转换,十进制常数可等价于带符号的整型或长整型,这取决于所需的常数的尺寸。八进制和十六进制常数可对应整型、无符号整型、长整型或无符号长整型,具体类型也取决于常数的大小。如果常数可用整型表示,则使用整型。如果常数值大于一个整型所能表示的最大值,但又小于整型位数所能表示的最大数,则使用无符号整型。同理,如果一个常数比无符号整型所表示的值还大,则它为长整型。如果需要,当然也可用无符号长整型。在一个常数后面加一个字母 l或L,则认为是长整型。如10L、79L、012L、0115L、0XAL、0x4fL等。2.3.2 整型变量前面已提到, C规定在程序中所有用到的变量都必须在程序中指定其类型,即“定义”。这是和BASIC、FORTRAN不同的,而与Pascal相似。[例2-1]main(){}int a,b,c,d;unsigned u;a=12; b=-24; u=10;c=a+u; d=b+u;printf("a+u=%d, b+u=%d\n",c,d);/*指定a,b,c,d 为整型变量*//*指定u为无符号整型变量 */运行结果为 :RUN a+u=22, b+u=-1418C语言程序设计下载可以看到不同类型的整型数据可以进行算术运算。在本例中是 int型数据与 unsingned int型数据进行相加减运算。2.4 实型数据2.4.1 实型常量实型常量又称浮点常量,是一个十进制表示的符号实数。符号实数的值包括整数部分、尾数部分和指数部分。实型常量的形式如下:[digits][.digits][E|e[+|-]digits]在此digits是一位或多位十进制数字(从 0~9)。 E(也可用e)是指数符号。小数点之前是整数部分,小数点之后是尾数部分,它们是可省略的。小数点在没有尾数时可省略。指数部分用 E 或e 开头,幂指数可以为负,当没有符号时视为正指数的基数为 10,如1.575E10表示为: 1.575×1010。在实型常量中不得出现任何空白符号。在不加说明的情况下,实型常量为正值。如果表示负值,需要在常量前使用负号。下面是一些实型常量的示例:15.75, 1.575E10, 1575e-2, -0.0025, -2.5e-3, 25E-4所有的实型常量均视为双精度类型。实型常量的整数部分为 0时可以省略,如下形式是允许的:.57, .0075e2, -.125, -.175E-2注意 字母E或e之前必须有数字,且E或e后面指数必须为整数,如e3、2.1e3.5、.e3、e等都是不合法的指数形式。2.4.2 实型变量实型变量分为单精度( float型)和双精度( double型)。对每一个实型变量都应再使用前加以定义。如:float x,y;double z;/*指定x,y为单精度实数*//*指定z为双精度实数*/在一般系统中,一个 float 型数据在内存中占 4个字节( 32位)一个 double型数据占 8个字节(64位)。单精度实数提供7位有效数字,双精度提供 15~16位有效数字,数值的范围随机器系统而异。值得注意的是,实型常量是 double型,当把一个实型常量赋给一个 float型变量时,系统会截取相应的有效位数。例如float a;a=111111.111;由于float型变量只能接收 7位有效数字,因此最后两位小数不起作用。如果将 a改为double型,则能全部接收上述 9位数字并存储在变量 a中。下载2.5 字符型数据2.5.1 字符常量第2章 数据类型、运算符和表达式19字符常量是指用一对单引号括起来的一个字符。如‘ a’,‘9’,‘!’。字符常量中的单引号只起定界作用并不表示字符本身。单引号中的字符不能是单引号(’)和反斜杠( \),它们特有的表示法在转义字符中介绍。在C语言中,字符是按其所对应的 ASCII码值来存储的,一个字符占一个字节。例如:字符 ASCII码值(十进制)!019ABab3348495765669798注意 字符'9'和数字9的区别,前者是字符常量,后者是整型常量,它们的含义和在计算机中的存储方式都截然不同。由于C语言中字符常量是按整数( short型)存储的,所以字符常量可以像整数一样在程序中参与相关的运算。例如:'a'-32;'A' + 32;'9'-9;2.5.2 字符串常量/* 执行结果 97-32 = 65/* 执行结果 65+32 = 97/* 执行结果 57-9 = 48*/*/*/字符串常量是指用一对双引号括起来的一串字符。双引号只起定界作用,双引号括起的字符串中不能是双引号( ")和反斜杠(\),它们特有的表示法在转义字符中介绍。例如:"China" ,"C program", "YES&NO", "33312-2341", "A"C语言中,字符串常量在内存中存储时,系统自动在字符串的末尾加一个“串结束标志”,即ASCII码值为 0的字符 NULL,常用 \0表示。因此在程序中,长度为 n个字符的字符串常量,在内存中占有n+1个字节的存储空间。例如,字符串 China有5个字符,作为字符串常量 "China"存储于内存中时,共占 6个字节,系统自动在后面加上 NULL字符,其存储形式为:C h i n a NULL要特别注意字符串与字符串常量的区别,除了表示形式不同外,其存储性质也不相同,字符'A'只占1个字节,而字符串常量 "A"占2个字节。20C语言程序设计2.5.3 转义字符下载转义字符是 C语言中表示字符的一种特殊形式。通常使用转义字符表示 ASCII码字符集中不可打印的控制字符和特定功能的字符,如用于表示字符常量的单撇号( '),用于表示字符串常量的双撇号( ")和反斜杠( \)等。转义字符用反斜杠 \后面跟一个字符或一个八进制或十六进制数表示。表 2-4给出了 C语言中常用的转义字符。表2-4 转义字符转 义 字 符\a\b\f\n\r\t\v\\\ \'\"\0\ddd\xhh意 义响铃(BEL)退格(BS)换页(FF)换行(LF)回车(CR)水平制表(HT)垂直制表 (VT)反斜杠问号字符单引号字符双引号字符空字符(NULL)任意字符任意字符ASCII码值(十进制)007008012010013009011092063039034000三位八进制二位十六进制字符常量中使用单引号和反斜杠以及字符常量中使用双引号和反斜杠时,都必须使用转义字符表示,即在这些字符前加上反斜杠。在C程序中使用转义字符 \ddd 或者\xhh可以方便灵活地表示任意字符。 \ddd为斜杠后面跟三位八进制数,该三位八进制数的值即为对应的八进制 ASCII码值。\x后面跟两位十六进制数,该两位十六进制数为对应字符的十六进制 ASCII码值。使用转义字符时需要注意以下问题:1) 转义字符中只能使用小写字母,每个转义字符只能看作一个字符。2) \v 垂直制表和 \f 换页符对屏幕没有任何影响,但会影响打印机执行响应操作。3) 在C程序中,使用不可打印字符时,通常用转义字符表示。2.5.4 符号常量C语言允许将程序中的常量定义为一个标识符,称为符号常量。符号常量一般使用大写英文字母表示,以区别于一般用小写字母表示的变量。符号常量在使用前必须先定义,定义的形式是:#define <符号常量名> <常量>例如:#define PI3.1415926#define TRUE 1#definr FALSE 0#define STAR '*'这里定义 PI、TRUE、FLASE、STAR为符号常量,其值分别为 3.1415926, 1,0,'*'。下载第2章 数据类型、运算符和表达式21#define是C语言的预处理命令,它表示经定义的符号常量在程序运行前将由其对应的常量替换。定义符号常量的目的是为了提高程序的可读性,便于程序的调试和修改。因此在定义符号常量名时,应使其尽可能地表达它所代表的常量的含义,例如前面所定义的符号常量名PI( ),表示圆周率 3.1415926。此外,若要对一个程序中多次使用的符号常量的值进行修改,只须对预处理命令中定义的常量值进行修改即可。2.5.5 字符变量字符变量用来存放字符常量,注意只能存放一个字符,不要以为在一个字符变量中可以放字符串。字符变量的定义形式如下:char c1, c2;它表示c1和c2为字符变量,各放一个字符。因此可以用下面语句对 c1、c2赋值:c1 = 'a';[例2-2]main(){c2 = 'b';}char c1,c2;c1=97; c2=98;printf("%c %c",c1,c2);c1、c2被指定为字符变量。但在第 3行中,将整数 97和98分别赋给 c1和c2,它的作用相当于以下两个赋值语句:c1='a'; c2='b';因为'a'和'b'的ASCII码为97和98。第4行将输出两个字符。 "%c"是输出字符的格式。程序输出:RUN a[例2-3]main()b{ char c1,c2;c1='a' ;c2='b';c1 = c1 - 32; c2 =c2 - 32;printf("%c %c",c1,c2);}运行结果为:RUN AB它的作用是将两个小写字母转换为大写字母。因为 'a'的ASCII码为97,而'A'为65,'b'为98,'B'为66。从ASCII代码表中可以看到每一个小写字母比大写字母的 ASCII码大 32。即'a'='A' +32。22C语言程序设计2.6 运算符下载C语言的内部运算符很丰富,运算符是告诉编译程序执行特定算术或逻辑操作的符号。 C语言有三大运算符:算术、关系与逻辑、位操作。另外, C还有一些特殊的运算符,用于完成一些特殊的任务。2.6.1 算术运算符表2-5列出了C语言中允许的算术运算符。在 C语言中,运算符“ + ”、“-”、“*”和“ /”的用法与大多数计算机语言的相同,几乎可用于所有 C语言内定义的数据类型。当“ /”被用于整数或字符时,结果取整。例如,在整数除法中, 10/3=3。一元减法的实际效果等于用 -1乘单个操作数,即任何数值前放置减号将改变其符号。模运算符“% ”在C语言中也同它在其它语言中的用法相同。切记,模运算取整数除法的余数,所以“%”不能用于 float和double类型。表2-5 算术运算符运 算 符-+*/作 用减法,也是一元减法加法乘法除法运 算 符%--++作 用模运算自减(减1)自增(增1)下面是说明 %用法的程序段。int x,y;x=10;y=3;printf("%d",x/y);printf("%d",x%y);x=1;y=2;printf("%d,%d",x/y,x%y);/* 显示 3 *//* 显示 1,整数除法的余数 *//* 显示 0,1 */最后一行打印一个 0和一个1,因为1/2整除时为 0,余数为1,故1%2取余数1。2.6.2 自增和自减C语言中有两个很有用的运算符,通常在其它计算机语言中是找不到它们的—自增和自减运算符, ++和--。运算符“ ++”是操作数加 1,而“--”是操作数减1,换句话说:x=x+1; 同++x;x=x-1; 同--x;自增和自减运算符可用在操作数之前,也可放在其后,例如: x=x+1;可写成 ++x;或x++;但在表达式中这两种用法是有区别的。自增或自减运算符在操作数之前, C语言在引用下载第2章 数据类型、运算符和表达式23操作数之前就先执行加 1或减1操作;运算符在操作数之后, C语言就先引用操作数的值,而后再进行加1或减1操作。请看下例:x=10;y=++x;此时,y=11。如果程序改为:x=10;y=x++;则y=10。在这两种情况下, x都被置为 11,但区别在于设置的时刻,这种对自增和自减发生时刻的控制是非常有用的。在大多数C编译程序中,为自增和自减操作生成的程序代码比等价的赋值语句生成的代码要快得多,所以尽可能采用加 1或减1运算符是一种好的选择。下面是算术运算符的优先级:最高 ++、---(一元减)*、/、%最低 +、-编译程序对同级运算符按从左到右的顺序进行计算。当然,括号可改变计算顺序。 C语言处理括号的方法与几乎所有的计算机语言相同:强迫某个运算或某组运算的优先级升高。2.6.3 关系和逻辑运算符关系运算符中的“关系”二字指的是一个值与另一个值之间的关系,逻辑运算符中的“逻辑”二字指的是连接关系的方式。因为关系和逻辑运算符常在一起使用,所以将它们放在一起讨论。关系和逻辑运算符概念中的关键是 True (真)和Flase(假)。C语言中,非 0为True,0为Flase。使用关系或逻辑运算符的表达式对 Flase和Ture分别返回值 0或1(见表2-6)。表2-6 关系和逻辑运算符关系运算符>>=<逻辑运算符&&||!含 义大于大于等于小于关系运算符<===!=含 义与或非含 义小于或等于等于不等于表2-6给出于关系和逻辑运算符,下面用 1和0给出逻辑真值表。关系和逻辑运算符的优先级比算术运算符低,即像表达式 10>1+12的计算可以假定是对表达式10>(1+12)的计算,当然,该表达式的结果为 Flase。在一个表达式中允许运算的组合。例如:10>5&&!(10<9)||3<=424C语言程序设计下载p0011这一表达式的结果为 True。q0110p&&q0010p||q0111!p1100下表给出了关系和逻辑运算符的相对优先级:最高 !>= <=== !=&&最低 ||同算术表达式一样,在关系或逻辑表达式中也使用括号来修改原计算顺序。切记,所有关系和逻辑表达式产生的结果不是 0就是 1,所以下面的程序段不仅正确而且将在屏幕上打印数值 1。int x;x=100;printf("%d",x>10);2.6.4 位操作符与其它语言不同, C语言支持全部的位操作符( Bitwise Operators)。因为C语言的设计目的是取代汇编语言,所以它必须支持汇编语言所具有的运算能力。位操作是对字节或字中的位(bit)进行测试、置位或移位处理,这里字节或字是针对 C标准中的char和int数据类型而言的。位操作不能用于 float、double、long double、void或其它复杂类型。表 2-7给出了位操作的操作符。位操作中的 AND、OR和NOT(1的补码)的真值表与逻辑运算等价,唯一不同的是位操作是逐位进行运算的。表2-7 位操作符操 作 符&|^下面是异或的真值表。含 义与(AND)或(OR)异或(XOR)操 作 符~>><<含 义1的补(NOT)右移左移P0110表2-8 异或的真值表q0011p^q0101下载第2章 数据类型、运算符和表达式25如表2-8所示,当且仅当一个操作数为 True时,异或的输出为 True,否则为 Flase。位操作通常用于设备驱动程序,例如调制解调器程序、磁盘文件管理程序和打印机驱动程序。这是因为位操作可屏蔽掉某些位,如奇偶校验位(奇偶校验位用于确保字节中的其它位不会发生错误通常奇偶校验位是字节的最高位)。通常我们可把位操作 AND作为关闭位的手段,这就是说两个操作数中任一为 0的位,其结果中对应位置为 0。例如,下面的函数通过调用函数 read_modem(),从调制解调器端口读入一个字符,并将奇偶校验位置成 0。[例2-4]Char get_char_from_modem(){char ch;ch=read_modem(); /*从调制解调器端口中得到一个字符 */return(ch&127);}字节的位8是奇偶位,将该字节与一个位 1到位 7为1、位 8为0的字节进行与操作,可将该字节的奇偶校验位置成 0。表达式 ch&127正是将 ch中每一位同 127数字的对应位进行与操作,结果ch的位8被置成了 0。在下面的例子中,假定 ch接收到字符"A"并且奇偶位已经被置位。奇偶位↓110000001 内容为‘A’的ch,其中奇偶校验位为 1011111111 二进制的 127执行与操作& 与操作= 010000001 去掉奇偶校验的‘ A’位操作OR与AND操作相反,可用来置位。任一操作数中为 1的位将结果的对应位置 1。如下所示,128|3的情况是:1000000 128的二进制0000011 3的二进制| 或操作= 1000011 结果异或操作通常缩写为 XOR,当且仅当做比较的两位不同时,才将结果的对应位置位。如下所示,异或操作 127^120的情况是:01111111 127 的二进制01111000 120的二进制^ 异或操作= 00000111 结果一般来说,位的 AND、OR和XOR操作通过对操作数运算,直接对结果变量的每一位分别处理。正是因为这一原因(还有其它一些原因),位操作通常不像关系和逻辑运算符那样用在条件语句中,我们可以用例子说明这一点:假定 X = 7,那么 x & & 8 为Tu r e ( 1 ) , 而 x & 8却为Flase(0)。记住,关系和逻辑操作符结果不是 0就是 1。而相似的位操作通过相应处理,结果可为任26C语言程序设计下载意值。换言之,位操作可以有 0或1以外的其它值,而逻辑运算符的计算结果总是 0或1。移位操作符 >>和<<将变量的各位按要求向或向左移动。右移语句通常形式是:variable >>右移位数左移语句是:variable<<左移位数当某位从一端移出时,另一端移入 0(某些计算机是送 1,详细内容请查阅相应 C编译程序用户手册)。切记:移位不同于循环,从一端移出的位并不送回到另一端去,移去的位永远丢失了,同时在另一端补 0。移位操作可对外部设备(如 D/A转换器)的输入和状态信息进行译码,移位操作还可用于整数的快速乘除运算。如表 2-9所示(假定移位时补 0),左移一位等效于乘 2,而右移一位等效于除以2。表2-9 用移位操作进行乘和除字 符 xx=7x<<1x<<3x<<2x>>1x>>2每个语句执行后的x000001110000111001110000110000000110000000011000x 的 值7141121929624每左移一位乘 2,注意x<<2后,原 x的信息已经丢失了,因为一位已经从一端出,每右移一位相当于被2除,注意,乘后再除时,除操作并不带回乘法时已经丢掉的高位。反码操作符为 ~。~的作用是将特定变量的各位状态取反,即将所有的 1位置成0,所有的 0位置成1。位操作符经常用在加密程序中,例如,若想生成一个不可读磁盘文件时,可以在文件上做一些位操作。最简单的方法是用下述方法,通过 1的反码运算,将每个字节的每一位取反。原字节00101100第一次取反码 11010011第二次取反码 00101100注意,对同一行进行连续的两次求反,总是得到原来的数字,所以第一次求反表示了字节的编码,第二次求反进行译码又得到了原来的值。可以用下面的函数 encode()对字符进行编码。[例2-5]char encode(ch)char ch;{return (~ch);}2.6.5 操作符C语言提供了一个可以代替某些 if-then-else语句的简便易用的操作符?。该操作符是三元下载的,其一般形式为:EXP1 EXE2:EXP3第2章 数据类型、运算符和表达式27EXP1,EXP2和EXP3是表达式,注意冒号的用法和位置。操作符“ ”作用是这样的,在计算 EXP1之后,如果数值为 True,则计算 EXP2,并将结果作为整个表达式的数值;如果 EXP1的值为 Flase,则计算 EXP3,并以它的结果作为整个表达式的值,请看下例:x=10;y=x>9 100:200;例中,赋给 y的数值是 100,如果x被赋给比9小的值, y的值将为200,若用if-else语句改写,有下面的等价程序:x=10;if(x>9) y=100;else y=200;有关C语言中的其它条件语句将在第 3章进行讨论。2.6.6 逗号操作符作为一个操作符,逗号把几个表达式串在一起。逗号操作符的左侧总是作为 void(无值),这意味着其右边表达式的值变为以逗号分开的整个表达式的值。例如:x=(y=3,y+1);这行将 3赋给y,然后将 4赋给 x,因为逗号操作符的优先级比赋值操作符优先级低,所以必须使用括号。实际上,逗号表示操作顺序。当它在赋值语句右边使用时,所赋的值是逗号分隔开的表中最后那个表达式的值。例如,y=10;x=(y=y-5,25/y);执行后,x的值是5,因为y的起始值是 10,减去5之后结果再除以25,得到最终结果。在某种意义上可以认为,逗号操作符和标准英语的 and是同义词。2.6.7 关于优先级的小结表2-10列出了C语言所有操作符的优先级,其中包括将在本书后面讨论的某些操作符。注意,所有操作符(除一元操作符和?之外)都是左结合的。一元操作符( *,&和-)及操作符“?”则为右结合。表2-10 C语言操作符的优先级最 高 级()[] →!~ ++ -- -(type) * & sizeof* / %+ -<< >><= >=== !=28C语言程序设计最低级2.7 表达式&^|&&|| = += -= *= /=,下载(续)表达式由运算符、常量及变量构成。 C语言的表达式基本遵循一般代数规则,有几点却是与C语言紧密相关的,以下将分别加以讨论。2.7.1 表达式中的类型转换混合于同一表达式中的不同类型常量及变量,应均变换为同一类型的量。 C语言的编译程序将所有操作数变换为与最大类型操作数同类型。变换以一次一操作的方式进行。具体规则如下:char ch;int i;float f;double d;result=(ch / i) + ( f * d ) - ( f + i );int double doubleint double doubledoubledouble图2-1 类型转换实例1) 所有char及short int 型量转为int型,所有float转换为double。2) 如操作数对中一个为 long double ,另一个转换为 long double 。① 要不然,一个为double,另一个转为 double。② 要不然,一个为 long,另一个转为 long 。③ 要不然,一个为unsigned,另一个转为 unsigned。一旦运用以上规则。每一对操作数均变为同类型。注意,规则 2)有几种必须依次应用的条件。图2-1示出了类型转换。首先, char ch转换成 int,且float f 转换成double;然后ch/i的结果转换成 double,因为 f*d是double;最后由于这次两个操作数都是 double,所以结果也是下载double.2.7.2 构成符cast第2章 数据类型、运算符和表达式29可以通过称为cast的构成符强迫一表达式变为特定类型。其一般形式为:(type )expression(type)是标准 C语言中的一个数据类型。例如,为确保表达式 x/2的结果具有类型 float,可写为:(float )x/2通常认为cast是操作符。作为操作符, cast是一元的,并且同其它一元操作符优先级相同。虽然cast在程序中用得不多,但有时它的使用的确很有价值。例如,假设希望用一整数控制循环,但在执行计算时又要有小数部分。[例2-6]main(){int i ;for (i+1;i<=100;++i)printf("%d/2 is :%f",i,(float)i/2);}若没有 cast(float),就仅执行一次整数除;有了 cast就可保证在屏幕上显示答案的小数部分。2.7.3 空格与括号为了增加可读性,可以随意在表达式中插入tab和空格符。例如,下面两个表达式是相同的。x=10/y*(127/x);x=10/y*(127/x);冗余的括号并不导致错误或减慢表达式的执行速度。我们鼓励使用括号,它可使执行顺序更清楚一些。例如,下面两个表达式中哪个更易读一些呢?x=y/2-34*temp&127;x=(y/2)-((34*temp)&127);2.7.4 C语言中的简写形式C语言提供了某些赋值语句的简写形式。例如语句:x=x+10;在C语言中简写形式是:x+=10 ;这组操作符对 +=通知编译程序将 X+10的值赋予 X。这一简写形式适于 C语言的所有二元操作符(需两个操作数的操作符)。在C语言中,variable=variable1 operator expression;30C语言程序设计与variable1 operator=expression相同。请看另一个例子:x=x-100;其等价语句是x-=100;简写形式广泛应用于专业 C语言程序中,希望读者能熟悉它。下载(共136张PPT)7.1 图的定义和术语7.6 最短路径7.5 有向无环图及其应用7.2 图的存储表示7.3 图的遍历7.4 图的连通性问题图是由一个顶点集 V 和一个弧集 R构成的数据结构。Graph = (V, R )其中,VR={| v,w∈V 且 P(v,w)}表示从 v 到 w 的一条弧,并称 v 为弧头,w 为弧尾。谓词 P(v,w) 定义了弧 的意义或信息。7.1 图的定义及术语:由于“弧”是有方向的,因此称由顶点集和弧集构成的图为有向图。EACBD例如:G1 = (V1, VR1)其中V1={A, B, C, D, E}VR1={, ,, , ,, }若 VR 必有 VR,则称 (v,w) 为顶点 v 和顶点 w 之间存在一条边。BCAFED由顶点集和边集构成的图称作无向图。例如: G2=(V2,VR2)V2={A, B, C, D, E, F}VR2={(A, B), (A, E),(B, E), (C, D), (D, F),(B, F), (C, F) }名词和术语网、子图完全图、稀疏图、稠密图邻接点、度、入度、出度路径、路径长度、简单路径、简单回路连通图、连通分量、强连通图、强连通分量生成树、生成森林ABECFAEABBC设图G=(V,{VR}) 和图 G =(V ,{VR }),且 V V, VR VR,则称 G 为 G 的子图。1597211132弧或边带权的图分别称作有向网或无向网。假设图中有 n 个顶点,e 条边,则含有 e=n(n-1)/2 条边的无向图称作完全图;含有 e=n(n-1) 条弧的有向图称作 有向完全图;若边或弧的个数 e假若顶点v 和顶点w 之间存在一条边,则称顶点v 和w 互为邻接点,例如:ID(B) = 3ID(A) = 2边(v,w) 和顶点v 和w 相关联。和顶点v 关联的边的数目定义为边的度。ACDFEB右侧图中顶点的出度: 以顶点v 为弧尾的弧的数目;ABECF对有向图来说,顶点的入度: 以顶点v为弧头的弧的数目。顶点的度(TD)=出度(OD)+入度(ID)例如:ID(B) = 2OD(B) = 1TD(B) = 3由于弧有方向性,则有入度和出度之分设图G=(V,{VR})中的一个顶点序列{ u=vi,0,vi,1, …, vi,m=w}中,(vi,j-1,vi,j) VR 1≤j≤m,则称从顶点u 到顶点w 之间存在一条路径。路径上边的数目称作路径长度。ABECF如:从A到F长度为 3 的路径{A,B,C,F}简单路径:指序列中顶点不重复出现的路径。简单回路:指序列中第一个顶点和最后一个顶点相同的路径。若图G中任意两个顶点之间都有路径相通,则称此图为连通图;若无向图为非连通图,则图中各个极大连通子图称作此图的连通分量。BACDFEBACDFE若任意两个顶点之间都存在一条有向路径,则称此有向图为强连通图。ABECFABECF对有向图,否则,其各个强连通子图称作它的强连通分量。假设一个连通图有 n 个顶点和 e 条边,其中 n-1 条边和 n 个顶点构成一个极小连通子图,称该极小连通子图为此连通图的生成树。对非连通图,则称由各个连通分量的生成树的集合为此非连通图的生成森林。BACDFE结构的建立和销毁插入或删除顶点对邻接点的操作对顶点的访问操作遍历插入和删除弧基本操作CreatGraph(&G, V, VR):// 按定义(V, VR) 构造图DestroyGraph(&G):// 销毁图结构的建立和销毁对顶点的访问操作LocateVex(G, u);// 若G中存在顶点u,则返回该顶点在// 图中“位置” ;否则返回其它信息。GetVex(G, v); // 返回 v 的值。PutVex(&G, v, value);// 对 v 赋值value。对邻接点的操作FirstAdjVex(G, v);// 返回 v 的“第一个邻接点” 。若该顶点//在 G 中没有邻接点,则返回“空”。NextAdjVex(G, v, w);// 返回 v 的(相对于 w 的) “下一个邻接// 点”。若 w 是 v 的最后一个邻接点,则// 返回“空”。插入或删除顶点InsertVex(&G, v);//在图G中增添新顶点v。DeleteVex(&G, v);// 删除G中顶点v及其相关的弧。插入和删除弧InsertArc(&G, v, w);// 在G中增添弧,若G是无向的,//则还增添对称弧。DeleteArc(&G, v, w);//在G中删除弧,若G是无向的,//则还删除对称弧。遍 历DFSTraverse(G, v, Visit());//从顶点v起深度优先遍历图G,并对每//个顶点调用函数Visit一次且仅一次。BFSTraverse(G, v, Visit());//从顶点v起广度优先遍历图G,并对每//个顶点调用函数Visit一次且仅一次。7.2 图的存储表示一、图的数组(邻接矩阵)存储表示二、图的邻接表存储表示三、有向图的十字链表存储表示四、无向图的邻接多重表存储表示Aij={0 (i,j) VR1 (i,j) VR一、图的数组(邻接矩阵)存储表示BACDFE定义:矩阵的元素为有向图的邻接矩阵为非对称矩阵ABECFtypedef struct ArcCell { // 弧的定义VRType adj; // VRType是顶点关系类型。// 对无权图,用1或0表示相邻否;// 对带权图,则为权值类型。InfoType *info; // 该弧相关信息的指针} ArcCell,AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];typedef struct { // 图的定义VertexType // 顶点信息vexs[MAX_VERTEX_NUM];AdjMatrix arcs; // 弧的信息int vexnum, arcnum; // 顶点数,弧数GraphKind kind; // 图的种类标志} MGraph;DBACFE二、图的邻接表存储表示A 1 4B 0 4 5C 3 5D 2 5E 0 1F 1 2 30 1 2 3 4 5有向图的邻接表1 4230 120 1 2 3 4ABCDE ABECF可见,在有向图的邻接表中不易找到指向该顶点的弧ABECD有向图的逆邻接表ABCDE303420 01234在有向图的邻接表中,对每个顶点,链接的是指向该顶点的弧typedef struct ArcNode {int adjvex; // 该弧所指向的顶点的位置struct ArcNode *nextarc;// 指向下一条弧的指针InfoType *info; // 该弧相关信息的指针} ArcNode;adjvex nextarc info弧的结点结构typedef struct VNode {VertexType data; // 顶点信息ArcNode *firstarc;// 指向第一条依附该顶点的弧} VNode, AdjList[MAX_VERTEX_NUM];data firstarc顶点的结点结构typedef struct {AdjList vertices;int vexnum, arcnum;int kind; // 图的种类标志} ALGraph;图的结构定义(邻接表)三、有向图的十字链表存储表示ABCABC0 1 2∧0 2 ∧ ∧0 12 1 ∧2 0 ∧ ∧弧的结点结构弧尾顶点位置 弧头顶点位置 弧的相关信息指向下一个有相同弧尾的结点指向下一个有相同弧头的结点typedef struct ArcBox { // 弧的结构表示int tailvex, headvex; InfoType *info;struct ArcBox *hlink, *tlink;} VexNode;tailvexheadvexhlinktlinkinfo顶点的结点结构顶点信息数据指向该顶点的第一条入弧指向该顶点的第一条出弧typedef struct VexNode { // 顶点的结构表示VertexType data;ArcBox *firstin, *firstout;} VexNode;datafirstinfirstouttypedef struct {VexNode xlist[MAX_VERTEX_NUM];// 顶点结点(表头向量)int vexnum, arcnum;//有向图的当前顶点数和弧数} OLGraph;有向图的结构表示(十字链表)四、无向图的邻接多重表存储表示typedef struct Ebox {VisitIf mark; // 访问标记int ivex, jvex;//该边依附的两个顶点的位置struct EBox *ilink, *jlink;InfoType *info; // 该边信息指针} EBox;边的结构表示typedef struct { // 邻接多重表VexBox adjmulist[MAX_VERTEX_NUM];int vexnum, edgenum;} AMLGraph;顶点的结构表示typedef struct VexBox {VertexType data;EBox *firstedge; // 指向第一条依附该顶点的边} VexBox;无向图的结构表示7.3 图的遍历从图中某个顶点出发游历图,访遍图中其余顶点,并且使图中的每个顶点仅被访问一次的过程。深度优先搜索广度优先搜索遍历应用举例从图中某个顶点V0 出发,访问此顶点,然后依次从V0的各个未被访问的邻接点出发深度优先搜索遍历图,直至图中所有和V0有路径相通的顶点都被访问到。一、深度优先搜索遍历图连通图的深度优先搜索遍历SG1SG2SG3W1、W2和W3 均为 V 的邻接点,SG1、SG2 和 SG3 分别为含顶点W1、W2和W3 的子图。访问顶点 V ;for (W1、W2、W3 )若该邻接点W未被访问,则从它出发进行深度优先搜索遍历。Vw1w3w2从上页的图解可见:1. 从深度优先搜索遍历连通图的过程类似于树的先根遍历;解决的办法是:为每个顶点设立一个 “访问标志 visited[w]”;2. 如何判别V的邻接点是否被访问?void DFS(Graph G, int v) {// 从顶点v出发,深度优先搜索遍历连通图 Gvisited[v] = TRUE; VisitFunc(v);for(w=FirstAdjVex(G, v);w!=0; w=NextAdjVex(G,v,w))if (!visited[w]) DFS(G, w);// 对v的尚未访问的邻接顶点w// 递归调用DFS} // DFS首先将图中每个顶点的访问标志设为 FALSE, 之后搜索图中每个顶点,如果未被访问,则以该顶点为起始点,进行深度优先搜索遍历,否则继续检查下一顶点。非连通图的深度优先搜索遍历void DFSTraverse(Graph G,Status (*Visit)(int v)) {// 对图 G 作深度优先遍历。VisitFunc = Visit;for (v=0; vvisited[v] = FALSE; // 访问标志数组初始化for (v=0; vif (!visited[v]) DFS(G, v);// 对尚未访问的顶点调用DFS}abchdekfgF F F F F F F F FTTTTTTTTTachdkfebgachkfedbg访问标志:访问次序:例如:0 1 2 3 4 5 6 7 8812345670二、广度优先搜索遍历图Vw1w8w3w7w6w2w5w4对连通图,从起始点V到其余各顶点必定存在路径。其中,V->w1, V->w2, V->w8的路径长度为1;V->w7, V->w3, V->w5的路径长度为2;V->w6, V->w4的路径长度为3。w1Vw2w7w6w3w8w5w4从图中的某个顶点V0出发,并在访问此顶点之后依次访问V0的所有未被访问过的邻接点,之后按这些顶点被访问的先后次序依次访问它们的邻接点,直至图中所有和V0有路径相通的顶点都被访问到。若此时图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。void BFSTraverse(Graph G,Status (*Visit)(int v)){for (v=0; vvisited[v] = FALSE; //初始化访问标志InitQueue(Q); // 置空的辅助队列Qfor ( v=0; vif ( !visited[v]) { // v 尚未访问}} // BFSTraverse… …visited[v] = TRUE; Visit(v); // 访问uEnQueue(Q, v); // v入队列while (!QueueEmpty(Q)) {DeQueue(Q, u);// 队头元素出队并置为ufor(w=FirstAdjVex(G, u); w!=0;w=NextAdjVex(G,u,w))if ( ! visited[w]) {visited[w]=TRUE; Visit(w);EnQueue(Q, w); // 访问的顶点w入队列} // if} // while三、遍历应用举例1. 求一条从顶点 i 到顶点 s 的简单路径2. 求两个顶点之间的一条路径长度最短的路径1. 求一条从顶点 i 到顶点 s 的简单路径abchdekfg求从顶点 b 到顶点 k 的一条简单路径。从顶点 b 出发进行深度优先搜索遍历例如:假设找到的第一个邻接点是a,且得到的结点访问序列为: b a d h c e k f g假设找到的第一个邻接点是c,则得到的结点访问序列为:b c h d a e k f g1. 从顶点 i 到顶点 s ,若存在路径,则从顶点 i 出发进行深度优先搜索,必能搜索到顶点 s 。2. 遍历过程中搜索到的顶点不一定是路径上的顶点。结论:3. 由它出发进行的深度优先遍历已经完成的顶点不是路径上的顶点。void DFSearch( int v, int s, char *PATH) {// 从第v个顶点出发递归地深度优先遍历图G,// 求得一条从v到s的简单路径,并记录在PATH中visited[v] = TRUE; // 访问第 v 个顶点for (w=FirstAdjVex(v); w!=0 ;w=NextAdjVex(v) )if (!visited[w]) DFSearch(w, s, PATH);}Append(PATH, getVertex(v)); // 第v个顶点加入路径&&!foundif (w=s) { found = TRUE; Append(PATH, w); }elseif (!found) Delete (PATH); // 从路径上删除顶点 v2. 求两个顶点之间的一条路径长度最短的路径若两个顶点之间存在多条路径,则其中必有一条路径长度最短的路径。如何求得这条路径 因此,求路径长度最短的路径可以基于广度优先搜索遍历进行,但需要修改链队列的结点结构及其入队列和出队列的算法。abchdekfg深度优先搜索访问顶点的次序取决于图的存储结构,而广度优先搜索访问顶点的次序是按“路径长度”渐增的次序。例如:求右图中顶点 3 至顶点 5 的一条最短路径。链队列的状态如下所示:Q.front3 1 2 4 7 5321475689Q.rear1) 将链队列的结点改为“双链”结点。即结点中包含next 和priou两个指针;2) 修改入队列的操作。插入新的队尾结点时,令其priou域的指针指向刚刚出队列的结点,即当前的队头指针所指结点;3) 修改出队列的操作。出队列时,仅移 动队头指针,而不将队头结点从链表中删除。typedef DuLinkList QueuePtr;void InitQueue(LinkQueue& Q) {Q.front = Q.rear = new QNode;Q.front->next = Q.rear->next = NULL;}void EnQueue( LinkQueue& Q, QelemType e ) {p = new QNode;p->data = e; p->next = NULL;p->priou = Q.front;Q.rear->next = p; Q.rear = p;}void DeQueue( LinkQueue& Q, QelemType& e ) {Q.front = Q.front->next; e = Q.front->data}7.4 图的连通性问题假设要在 n 个城市之间建立通讯联络网,则连通 n 个城市只需要修建 n-1条线路,如何在最节省经费的前提下建立这个通讯网?问题:构造网的一棵最小生成树,即: 在 e 条带权的边中选取 n-1 条边(不构成回路),使“权值之和”为最小。算法二:(克鲁斯卡尔算法)该问题等价于:算法一:(普里姆算法)取图中任意一个顶点 v 作为生成树的根,之后往生成树上添加新的顶点 w。在添加的顶点 w 和已经在生成树上的顶点v 之间必定存在一条边,并且该边的权值在所有连通顶点 v 和 w 之间的边中取值最小。之后继续往生成树上添加顶点,直至生成树上含有 n-1 个顶点为止。普里姆算法的基本思想:abcdegf195141827168213127例如:aedcbgf148531621所得生成树权值和= 14+8+3+5+16+21 = 67在生成树的构造过程中,图中 n 个顶点分属两个集合:已落在生成树上的顶点集 U 和尚未落在生成树上的顶点集V-U ,则应在所有连通U中顶点和V-U中顶点的边中选取权值最小的边。一般情况下所添加的顶点应满足下列条件:UV-U设置一个辅助数组,对当前V-U集中的每个顶点,记录和顶点集U中顶点相连接的代价最小的边:struct {VertexType adjvex; // U集中的顶点序号VRType lowcost; // 边的权值} closedge[MAX_VERTEX_NUM];abcdegf195141827168213127aedcbaaa19141814例如:e12ee8168d3dd7213c5519 m m 14 m 1819 5 7 12 m mm 5 3 m m mm 7 3 8 21 m14 12 m 8 m 16m m m 21 m 2718 m m m 16 27void MiniSpanTree_P(MGraph G, VertexType u) {//用普里姆算法从顶点u出发构造网G的最小生成树k = LocateVex ( G, u );for ( j=0; jif (j!=k)closedge[j] = { u, G.arcs[k][j].adj };closedge[k].lowcost = 0; // 初始,U={u}for (i=0; i}继续向生成树上添加顶点;k = minimum(closedge);// 求出加入生成树的下一个顶点(k)printf(closedge[k].adjvex, G.vexs[k]);// 输出生成树上一条边closedge[k].lowcost = 0; // 第k顶点并入U集for (j=0; j//修改其它顶点的最小边if (G.arcs[k][j].adj < closedge[j].lowcost)closedge[j] = { G.vexs[k], G.arcs[k][j].adj };具体做法: 先构造一个只含 n 个顶点的子图 SG,然后从权值最小的边开始,若它的添加不使SG 中产生回路,则在 SG 上加上这条边,如此重复,直至加上 n-1 条边为止。考虑问题的出发点: 为使生成树上边的权值之和达到最小,则应使生成树中每一条边的权值尽可能地小。克鲁斯卡尔算法的基本思想:abcdegf195141827168213ae12dcbgf7148531621例如:7121819算法描述:构造非连通图 ST=( V,{ } );k = i = 0; // k 计选中的边数while (k++i;检查边集 E 中第 i 条权值最小的边(u,v);若(u,v)加入ST后不使ST中产生回路,则 输出边(u,v); 且 k++;}普里姆算法克鲁斯卡尔算法时间复杂度O(n2)O(eloge)稠密图稀疏图算法名适应范围比较两种算法7.5 有向无环图及其应用7.5.1 拓扑排序7.5.2 关键路径7.5.1 拓扑排序问题:假设以有向图表示一个工程的施工图或程序的数据流图,则图中不允许出现回路。检查有向图中是否存在回路的方法之一,是对有向图进行拓扑排序。何谓“拓扑排序”?对有向图进行如下操作:按照有向图给出的次序关系,将图中顶点排成一个线性序列,对于有向图中没有限定次序关系的顶点,则可以人为加上任意的次序关系。例如:对于下列有向图BDAC可求得拓扑有序序列:A B C D 或 A C B D由此所得顶点的线性序列称之为拓扑有序序列BDAC反之,对于下列有向图不能求得它的拓扑有序序列。因为图中存在一个回路 {B, C, D}如何进行拓扑排序?一、从有向图中选取一个没有前驱的顶点,并输出之;重复上述两步,直至图空,或者图不空但找不到无前驱的顶点为止。二、从有向图中删去此顶点以及所有以它为尾的弧;abcghdfeabhcdgfe在算法中需要用定量的描述替代定性的概念没有前驱的顶点 入度为零的顶点删除顶点及以它为尾的弧 弧头顶点的入度减1取入度为零的顶点v;while (v<>0) {printf(v); ++m;w:=FirstAdj(v);while (w<>0) {inDegree[w]--;w:=nextAdj(v,w);}取下一个入度为零的顶点v;}if m算法描述为避免每次都要搜索入度为零的顶点,在算法中设置一个“栈”,以保存“入度为零”的顶点。CountInDegree(G,indegree);//对各顶点求入度InitStack(S);for ( i=0; iif (!indegree[i]) Push(S, i);//入度为零的顶点入栈count=0; //对输出顶点计数while (!EmptyStack(S)) {Pop(S, v); ++count; printf(v);for (w=FirstAdj(v); w; w=NextAdj(G,v,w)){--indegree(w); // 弧头顶点的入度减一if (!indegree[w]) Push(S, w);//新产生的入度为零的顶点入栈}}if (count7.5.2 关键路径问题:假设以有向网表示一个施工流图,弧上的权值表示完成该项子工程所需时间。问:哪些子工程项是“关键工程”?即:哪些子工程项将影响整个工程的完成期限的。整个工程完成的时间为:从有向图的源点到汇点的最长路径。abcdefghk64521187244例如:“关键活动”指的是:该弧上的权值增加 将使有向图上的最长路径的长度增加。源点汇点6174如何求关键活动?“事件(顶点)” 的 最早发生时间 ve(j)ve(j) = 从源点到顶点j的最长路径长度;“事件(顶点)” 的 最迟发生时间 vl(k)vl(k) = 从顶点k到汇点的最短路径长度;假设第 i 条弧为 则 对第 i 项活动言“活动(弧)”的 最早开始时间 ee(i)ee(i) = ve(j);“活动(弧)”的 最迟开始时间 el(i)el(i) = vl(k) – dut();事件发生时间的计算公式:ve(源点) = 0;ve(k) = Max{ve(j) + dut()}vl(汇点) = ve(汇点);vl(j) = Min{vl(k) – dut()}abcdefghk6452118724400000000064571157151418181818181818181818161486610807拓扑有序序列: a - d - f - c - b - e - h - g - k0645771514181814161078660000645777151414160236688710算法的实现要点:显然,求ve的顺序应该是按拓扑有序的次序;而 求vl的顺序应该是按拓扑逆序的次序;因为 拓扑逆序序列即为拓扑有序序列的逆序列,因此 应该在拓扑排序的过程中,另设一个“栈”记下拓扑有序序列。 1. 熟悉图的各种存储结构及其构造算法,了解实际问题的求解效率与采用何种存储结构和算法有密切联系。 2. 熟练掌握图的两种搜索路径的遍历:遍历的逻辑定义、深度优先搜索和广度优先搜索的算法。在学习中应注意图的遍历算法与树的遍历算法之间的类似和差异。 3. 应用图的遍历算法求解各种简单路径问题。 4. 理解教科书中讨论的各种图的算法。7.6 最短路径求从某个源点到其余各点的最短路径每一对顶点之间的最短路径求从源点到其余各点的最短路径的算法的基本思想:依最短路径的长度递增的次序求得各条路径源点v1…其中,从源点到顶点v的最短路径是所有最短路径中长度最短者。v2在这条路径上,必定只含一条弧,并且这条弧的权值最小。下一条路径长度次短的最短路径的特点:路径长度最短的最短路径的特点:它只可能有两种情况:或者是直接从源点到该点(只含一条弧); 或者是,从源点经过顶点v1,再到达该顶点(由两条弧组成)。其余最短路径的特点:再下一条路径长度次短的最短路径的特点:它可能有三种情况:或者是,直接从源点到该点(只含一条弧); 或者是,从源点经过顶点v1,再到达该顶点(由两条弧组成);或者是,从源点经过顶点v2,再到达该顶点。它或者是直接从源点到该点(只含一条弧); 或者是,从源点经过已求得最短路径的顶点,再到达该顶点。求最短路径的迪杰斯特拉算法:一般情况下,Dist[k] = <源点到顶点 k 的弧上的权值>或者 = <源点到其它顶点的路径长度>+ <其它顶点到顶点 k 的弧上的权值>设置辅助数组Dist,其中每个分量Dist[k] 表示 当前所求得的从源点到其余各顶点 k 的最短路径。1)在所有从源点出发的弧中选取一条权值最小的弧,即为第一条最短路径。2)修改其它各顶点的Dist[k]值。假设求得最短路径的顶点为u,若 Dist[u]+G.arcs[u][k]则将 Dist[k] 改为 Dist[u]+G.arcs[u][k]V0和k之间存在弧V0和k之间不存在弧其中的最小值即为最短路径的长度。求每一对顶点之间的最短路径弗洛伊德算法的基本思想是:从 vi 到 vj 的所有可能存在的路径中,选出一条长度最短的路径若存在,则存在路径{vi,vj}// 路径中不含其它顶点若,存在,则存在路径{vi,v1,vj}// 路径中所含顶点序号不大于1若{vi,…,v2}, {v2,…,vj}存在,则存在一条路径{vi, …, v2, …vj}// 路径中所含顶点序号不大于2…依次类推,则 vi 至 vj 的最短路径应是上述这些路径中,路径长度最小者。习题与练习一、基本知识题1. 图的逻辑结构特点是什么?什么是无向图和有向图?什么是子图?什么是网络?2. 什么是顶点的度?什么是路径?什么是连通图和非连通图?什么是非连通图的连通分量?3. 给出图6.25所示的无向图G的邻接矩阵和邻接表两种存储结构。图6.25 一个无向图G4. 假设图的顶点是A、B……请根据下面的邻接矩阵画出相应的无向图或有向图。(a)(b)图6.26 一个无向图G5. 分别给出图6.26所示G图的深度优先搜索和广度优先搜索得到的顶点访问序列。图6.27 一个带权连通图G6. 应用prim算法求图6.27所示带权连通图的最小生成树。图6.28 一个有向图G7. 写出图6.28所示有向图的拓朴排序序列。二、算法设计题1. 如图6.29所示图G,试给出其对应的邻接表,并写出深度优先算法。2. 如图6.29所示图G,试给出其对应的邻接矩阵,并写出广度优先算法。3. 编写一个函数通过与用户交互建立一个有向图的邻接表。4. 编写一个无向图的邻接矩阵转换成邻接表的算法。图6.29 一个无向图G5. 已知一个有n个顶点的有向图的邻接表,设计算法分别实现1) 求出图中每个顶点的出度。2) 求出图中每个顶点的入度。3) 求出图中出度最大的一个顶点,输出其顶点序号。4) 计算图中出度为0的顶点个数。返回习题解答一、基本知识题答案1. 答:图是比树更为复杂的一种非线性数据结构,在图结构中,每个结点都可以和其它任何结点相连接。无向图:对于一个图G,若边集合E(G)为无向边的集合,则称该图为无向图。有向图:对于一个图G,若边集合E(G)为有向边的集合,则称该图为有向图。子图:设有两个图G =(V,E)和G’=(V’,E’),若V(G’)是V(G)的子集,且E(G’)是E(G)的子集,则称G’是G的子图(Subgraph)。网络:有些图,对应每条边有一相应的数值,这个数值叫做该边的权。边上带权的图称为带权图,也称为网络。2. 答:顶点的度:图中与每个顶点相连的边数,叫该顶点的度。在一个图中,若从某顶点Vp出发,沿一些边经过顶点V1,V2,…,Vm到达,Vq,则称顶点序列(Vp, V1,V2,…,Vm, Vq)为从Vp到Vq的路径。在无向图中,如果从顶点Vi到顶点Vj之间有路径,则称这两个顶点是连通的。如果图中任意一对顶点都是连通的,则称此图是连通图。非连通图的连通分量:非连通图的每一个连通的部分叫连通分量。3. 答:图G所对应的邻接矩阵如下:图G所对应的邻接表如下:4. 答:(a)所对应的无向图如下图(a)所示,(b)所对应的有向图如下图(b)所示:(a)(b)5. 答:深度优先搜索得到的顶点访问序列:0、1、3、7、8、4、9、5、6、2;广度优先搜索得到的顶点访问序列:0、1、2、3、4、5、6、7、8、9。6. 答:该图的最小生成树如下:7. 答:该有向图的拓朴排序序列为:3、1、4、5、2、6。二、算法设计题答案1. 解:该图对应的邻接表如下:深度优先算法:void dfsgraph(adjlist adj, int n)/*深度优先遍历整个图*/{int i;for(i=1;i<=n;i++)visited[i]=0;for(i=1;i<=n;i++)if(!visited[i])dfs(adj,i);}void dfs(adjlist adj,int v)/*以v为出发点,对图进行dfs遍历*/{ struct edgenode *p;visited[v]=1;printf("%d",v);p=adj[v]→link;while(p!=NULL){ if(visited[p→adjvex]==0)dfs(adjlist,p→adjvex);p=p→next;}}2. 解:该图对应的邻接矩阵如下:广度优先算法:void bfsgraph(int adjarray[n][n],int n)/*广度优先遍历整个图*/{int i;for(i=0;ivisited[i]=0;for(i=0;iif(!visited[i])bfs(adjarray,i);}void bfs(int adjarray[][],int v)/*以v为出发点,对图进行bfs遍历*/{int i,j;queue q;printf("%d",v);visited[v]=1;enqueue(&q,v);while(!queueemty(&q)){i=dequeue(&q);for(j=0;jif(adjarray[i][j]==1 && !visited[j]){printf("%d",j);visited[j]=1;enqueue(&q,j);}}}3. 解:实现本题功能的算法如下:void creategraph(adjlist g){int e,i,s,d,n;struct edgenode *p ;printf("请输入结点数(n)和边数(e):\n");scanf("%d,%d",&n,&e);for(i=1;i<=n;i++){printf("\n请输入第%d个顶点信息:",i);scanf("%c",&g[i].data);g[i].link=NULL;}for(i=1;i<=e;i++){ printf("\n请输入第%d条边起点序号,终点序号:",i);scanf("%d,%d",&s,&d);p=(struct edgenode *)malloc(sizeof(edgenode));p→adjvex=d;p→next=g[s].link;g[s].link=p;}}4. 解:本题的算法思想是:逐个扫描邻接矩阵的各个元素,如第i行第j列的元素为1,则相应的邻接表的第i个单链表上增加一个j结点。实现本题功能的算法如下:void transform(int adjarray[n][n],adjlist adj){int i,j;edgenode *p;for(i=0;i{adj[i].data=i;adj[i].link=NULL;}for(i=0;ifor(j=0;j{ if(adjarray[i][j]==1){ p=(edgenode *)malloc(sizeof(edgenode));p→adjvex=j;p→next=adj[i].link;adj[i].link=p;}}}5.解:(1) 本题的算法思想是:计算出邻接表中第i个单链表的结点数,即为i顶点的出度。求顶点的出度数的算法如下:int outdegree(adjlist adj,int v){ int degree=0;edgenode *p;p=adj[v].link;while(p!=NULL){ degree++;p=p->next;}return degree;}void printout(adjlist adj,int n){int i,degree;printf("The outdegrees are:\n");for(i=0;i{degree=outdegree(adj,i);printf("(%d,%d)",i,degree);}}(2)本题的算法思想是:计算出整个邻接表中所具有的结点为i的结点数,这就是i顶点的入度。求顶点的入度数的算法:int indegree(adjlist adj,int n,int v){int i,j,degree;edgenode *p;for(i=0;i{p=adj[i].link;while(p!=NULL){if(p->adjvex==v)degree++;p=p->next;}}return degree;}void printin(adjlist adj,int n){int i,degree;printf("The indegrees are:\n");for(i=0;i{degree=indegree(adj,n,i);printf("(%d,%d)",i,degree);}}(3)求最大出度的算法:void maxoutdegree(adjlist adj,int n){int maxdegree=0,maxv=0, degree, i;for(i=0;i{degree=outdegree(adj,i);if(degree>maxdegree){maxdegree=degree;maxv=i;}}printf("maxoutdegree = %d,maxvertex = %d",maxdegree,maxv);}(4)求出度数为0的顶点数的算法: int outzero(adjlist adj,int n){int num=0,i;for(i=0;i{if(outdegree(adj,i)==0)num++;}return num;}返回 武汉大学年硕士生指导教师资格情况表姓名 性别 出生年月日 民族行政职务 专业技术职务 聘任时间 党派1.最后学历(最后学位)2.最后学历毕业年月3.最后学历毕业学校4.最后学历毕业专业 国内国外所在一级学科名称 所在二级学科名称主要研究方向首次硕导年月 是否兼职专家主要学术兼职(职务)是否学科评议组成员(国务院、省) 学科评议组名称工作单位(含院/所/中心)兼职专家工作单位※通信地址邮政编码 办公室电话住宅电话 Email移动电话 Fax注:1.本表一律用Word格式编辑,宋体小四号字A4纸张,不得手填。2.※兼职专家的联系地址、电话号码请填写原单位。1、近三年内承担的本科生和研究生教学任务年 度 教 学 任 务2001年2002年2003年2.近三年科研成果及获奖情况:具有代表性的论 文、专著和获奖项目 序号 成果(论文、专著获奖项目)名称 成果鉴定、颁奖部门及奖励类别、等级或发表刊物与出版单位、时间 本人署名次序12345673、目前主持或承担的科研课题课 题 名 称 下达课题单位 是否主持 经费额度4、近三年指导研究生情况年 度 硕 士2001年2002年2003年学位评定分委员会(学位工作小组)审核意见: 主席(组长)签字: 年 月 日 展开更多...... 收起↑ 资源列表 CHAP01.ppt CHAP02.ppt CHAP03.ppt CHAP04.ppt CHAP05.ppt CHAP06.ppt CHAP07.ppt CHAP09.ppt CHAP10.ppt CHAP11.ppt CHAP12.ppt C语言数据结构.doc C语言数据结构.pdf lsdzg.doc sjjgsyzd.doc 《数据结构(C语言描述)》(王路群 主编)源代码.doc 笔记.doc 考试复习.doc