数据结构教程ppt和复习资料

文档属性

名称 数据结构教程ppt和复习资料
格式 zip
文件大小 3.0MB
资源类型 教案
版本资源 通用版
科目 信息技术(信息科技)
更新时间 2010-11-04 22:55:00

文档简介

(共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棵子树
A
B
C
D
E
F
G
H
I
J
M
K
L
A( )
T1
T3
T2
树根
例如:
B(E, F(K, L)),
C(G),
D(H, I, J(M))
(1) 有确定的根;
(2) 树根和子树根之间为有向关系。
有向树:
有序树:
子树之间存在确定的次序关系。
无序树:
子树之间不存在确定的次序关系。
基 本 术 语
结点:
结点的度:
树的度:
叶子结点:
分支结点:
数据元素+若干指向子树的分支
分支的个数
树中所有结点的度的最大值
度为零的结点
度大于零的结点
D
H
I
J
M
(从根到结点的)路径:
孩子结点、双亲结点、
兄弟结点、堂兄弟
祖先结点、子孙结点
结点的层次:
树的深度:
由从根到该结点所经分支和结点构成
A
B
C
D
E
F
G
H
I
J
M
K
L
假设根结点的层次为1,第l 层的结点的子树根结点的层次为l+1
树中叶子结点所在的最大层次
任何一棵非空树是一个二元组
Tree = (root,F)
其中:root 被称为根结点,
F 被称为子树森林
森林:
是m(m≥0)棵互
不相交的树的集合
A
root
B
E
F
K
L
C
G
D
H
I
J
M
F
对比树型结构和线性结构的结构特点
线性结构
树型结构
第一个数据元素
(无前驱)
根结点
(无前驱)
最后一个数据元素
(无后继)
多个叶子结点
(无后继)
其它数据元素
(一个前驱、
一个后继)
其它数据元素
(一个前驱、
多个后继)
6.2
二叉树的类型定义
二叉树或为空树;或是由一个根结点加上两棵分别称为左子树和右子树的、互不相交的二叉树组成。
A
B
C
D
E
F
G
H
K
根结点
左子树
右子树
二叉树的五种基本形态:
N
空树
只含根结点
N
N
N
L
R
R
右子树为空树
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 的结点一一对应。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
a
b
c
d
e
f
g
h
i
j
性质 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 TElemType
SqBiTree[MAX_TREE_SIZE];
// 0号单元存储根结点
SqBiTree bt;
一、 二叉树的顺序存储表示
例如:
A
B
C
D
E
F
A B D C E F
0 1 2 3 4 5 6 7 8 9 10 11 12 13
1
4
0
13
2
6
二、二叉树的链式存储表示
1. 二叉链表
2.三叉链表
3.双亲链表
4.线索链表
A
D
E
B
C
F







root
lchild data rchild
结点结构:
1. 二叉链表
typedef struct BiTNode { // 结点结构
TElemType data;
struct BiTNode *lchild, *rchild;
// 左右孩子指针
} BiTNode, *BiTree;
lchild data rchild
结点结构:
C 语言的类型描述如下:
A
D
E
B
C
F







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 语言的类型描述如下:
0
1
2
3
4
5
6
data parent
结点结构:
3.双亲链表
LRTag
L
R
R
R
L
A
B
D
C
E
F
typedef struct BPTNode { // 结点结构
TElemType data;
int *parent; // 指向双亲的指针
char LRTag; // 左、右孩子标志域
} BPTNode
typedef struct BPTree{ // 树结构
BPTNode nodes[MAX_TREE_SIZE];
int num_node; // 结点数目
int root; // 根结点的位置
} BPTree
6.4
二叉树的遍历
一、问题的提出
二、先左后右的遍历算法
三、算法的递归描述
四、中序遍历算法的非递归描述
五、遍历算法的应用举例
顺着某一条搜索路径巡访二叉树
中的结点,使得每个结点均被访问一
次,而且仅被访问一次。
一、问题的提出
“访问”的含义可以很广,如:输出结
点的信息等。
“遍历”是任何类型均有的操作,
对线性结构而言,只有一条搜索路
径(因为每个结点均只有一个后继),
故不需要另加讨论。而二叉树是非
线性结构,
每个结点有两个后继,
则存在如何遍历即按什么样的搜索
路径遍历的问题。
对“二叉树”而言,可以有三条搜索路径:
1.先上后下的按层次遍历;
2.先左(子树)后右(子树)的遍历;
3.先右(子树)后左(子树)的遍历。
二、先左后右的遍历算法
先(根)序的遍历算法
中(根)序的遍历算法
后(根)序的遍历算法


子树

子树
若二叉树为空树,则空操作;否则,
(1)访问根结点;
(2)先序遍历左子树;
(3)先序遍历右子树。
先(根)序的遍历算法:
若二叉树为空树,则空操作;否则,
(1)中序遍历左子树;
(2)访问根结点;
(3)中序遍历右子树。
中(根)序的遍历算法:
若二叉树为空树,则空操作;否则,
(1)后序遍历左子树;
(2)后序遍历右子树;
(3)访问根结点。
后(根)序的遍历算法:
A
B
C
D
E
F
G
H
K
例如:
先序序列:
中序序列:
后序序列:
A B C D E F G H K
B D C A E H G K F
D 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); // 处理访问任务
else
if(!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_iter
void Inorder_I(BiTree T, void (*visit)
(TelemType& e)){
Stack *S;
t = GoFarLeft(T, S); // 找到最左下的结点
while(t){
visit(t->data);
if (t->rchild)
else
if ( !StackEmpty(S )) t = Pop(S); // 退栈
else t = NULL; // 栈空表明遍历结束
} // while
}// Inorder_I
t = 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指向该结点并返回true
if (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
}//if
else 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
} // CountLeaf
int 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
} // CountLeaf
3、求二叉树的深度(后序遍历)
算法基本思想:
从二叉树深度的定义可知,二叉树的深度应为其左、右子树深度的最大值加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;
} // CopyTree
A
B
C
D
E
F
G
H
K
^ D ^
C ^
^ B
^ H ^
^ K ^
G
F ^
E ^
A
例如:下列二叉树的复制过程如下:
newT
5、建立二叉树的存储结构
不同的定义方法相应有不同的存储结构的建立算法
以字符串的形式 根 左子树 右子树
定义一棵二叉树
例如:
A
B
C
D
以空白字符“ ”表示
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; } // CreateBiTree
A B C D
A
B
C
D
上页算法执行过程举例如下:
A
T
B
C
D
^
^
^
^
^
按给定的表达式建相应二叉树
由先缀表示式建树
例如:已知表达式的先缀表示式
-×+ a b c / d e
由原表达式建树
例如:已知表达式 (a+b)×c – d/e
对应先缀表达式 -×+ a b c / d e的二叉树
a
b
c
d
e
-
×
+
/
特点:
操作数为叶子结点,
运算符为分支结点
scanf(&ch);
if ( In(ch, 字母集 )) 建叶子结点;
else { 建根结点;
递归建左子树;
递归建右子树;
}
由先缀表示式建树的算法的基本操作:
a+b
(a+b)×c – d/e
a+b×c
分析表达式和二叉树的关系:
a
b
+
a
b
c
×
+
a
b
c
×
+
(a+b)×c
a
b
c
d
e
-
×
+
/
基本操作:
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;}
} // while
Pop(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 g
c b d a e g f
例如:
a
a
b
b
c
c
d
d
e
e
f
f
g
g
a
b
c
d
e
f
g
^
^
^
^
^
^
^
^
先序序列中序序列
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
线索二叉树
何谓线索二叉树?
线索链表的遍历算法
如何建立线索链表?
一、何谓线索二叉树?
遍历二叉树的结果是,
求得结点的一个线性序列。
A
B
C
D
E
F
G
H
K
例如:
先序序列:
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 ^
^ B
E ^
对线索链表中结点的约定:
在二叉链表的结点中增加两个标志域,
并作如下规定:
若该结点的左子树不空,
则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==T
while (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
} // InThreading
Status 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 树和森林
的表示方法
树的三种存储结构
一、双亲表示法
二、孩子链表表示法
三、树的二叉链表(孩子-兄弟)
存储表示法
A
B
C
D
E
F
G
r=0
n=6
0 A -1
1 B 0
2 C 0
3 D 0
4 E 2
5 F 2
6 G 5
data 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=0
n=6
data firstchild
A
B
C
D
E
F
G
0 A -1
1 B 0
2 C 0
3 D 0
4 E 2
5 F 2
6 G 4
6
4 5
1 2 3
二、孩子链表表示法:
-1
0
0
0
2
2
4
typedef struct CTNode {
int child;
struct CTNode *nextchild;
} *ChildPtr;
孩子结点结构:
child nextchild
C语言的类型描述:
typedef struct {
Elem data;
ChildPtr firstchild;
// 孩子链的头指针
} CTBox;
双亲结点结构
data firstchild
typedef struct {
CTBox nodes[MAX_TREE_SIZE];
int n, r;
// 结点数和根结点的位置
} CTree;
树结构:
A
B
C
D
E
F
G
root
A
B
C
E D
F
G
A
B
C
E D
F
G
三、树的二叉链表 (孩子-兄弟)存储表示法
root
typedef 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)。
T1
T11,T12,…,T1m
T2,…,Tn
LBT
RBT
root
由此,树和森林的各种操作均可与二叉树的各种操作相对应。
应当注意的是,和树对应的二叉树,其左、右子树的概念
已改变为: 左是孩子,右是兄弟
6.7
树和森林的遍历
一、树的遍历
二、森林的遍历
三、树的遍历的应用
树的遍历可有三条搜索路径:
按层次遍历:
先根(次序)遍历:
后根(次序)遍历:
若树不空,则先访问根结点,然后依次先根遍历各棵子树。
若树不空,则先依次后根遍历各棵子树,然后访问根结点。
若树不空,则自上而下自左至右访问树中每个结点。
层次遍历时顶点的访问次序:
A
B C D
E F G
H
I J K
先根遍历时顶点的访问次序:
A B E F C D G H I J K
后根遍历时顶点的访问次序:
E F B C I J K H G D A
A B C D E F G H I J K
B C D
E F G
H
I J K
1。森林中第一棵树的根结点;
2。森林中第一棵树的子树森林;
3。森林中其它树构成的森林。
可以分解成三部分:
森林
若森林不空,则
访问森林中第一棵树的根结点;
先序遍历森林中第一棵树的子树森林;
先序遍历森林中(除第一棵树之外)其
余树构成的森林。
先序遍历
森林的遍历
即:依次从左至右对森林中的每一棵树进行先根遍历。
中序遍历
若森林不空,则
中序遍历森林中第一棵树的子树森林;
访问森林中第一棵树的根结点;
中序遍历森林中(除第一棵树之外)其 余树构成的森林。
即:依次从左至右对森林中的每一棵树进行后根遍历。
树的遍历和二叉树遍历的对应关系 ?
先根遍历
后根遍历

二叉树
森林
先序遍历
先序遍历
中序遍历
中序遍历
设树的存储结构为孩子兄弟链表
typedef struct CSNode{
Elem data;
struct CSNode *firstchild, *nextsibling;
} CSNode, *CSTree;
一、求树的深度
二、输出树中所有从根到叶子的路径
三、建树的存储结构
int TreeDepth( Ctree T )
{ // T 是树的孩子链表存储结构,
// 返回该树的深度
if ( T.n == 0) return 0;
else
return 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;
}
二、输出树中所有从根到叶子的路径的算法:
A
B C D
E F G
H
I J K
例如:对左图所示的树,其输出结果应为:
A B E
A B F
A C
A D G H I
A D G H J
A D G H K
void 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)的形式自上而下、自左而右依次输入树的各边,建立树的孩子-兄弟链表。
A
B
C
D
E
F
G
例如:
对下列所示树的输入序列应为:
(‘#’, ‘A’)
(‘A’, ‘B’)
(‘A’, ‘C’)
(‘A’, ‘D’)
(‘C’, ‘E’)
(‘C’, ‘F’)
(‘E’, ‘G’)
A
B
C
D
(‘#’, ‘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 叉树中,必存在一棵其带权路径
长度取最小值的树,称为“最优树”。
例如:
2
7 9
7
5
4
9
2
WPL(T)= 7 2+5 2+2 3+4 3+9 2 =60
WPL(T)= 7 4+9 4+5 3+4 2+2 1 =89
5
4
根据给定的 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 }
5
6
2
7
5
2
7
6
9
7
6
7
13
9
5
2
7
6
7
13
9
5
2
7
9
5
2
7
16
6
7
13
29
0
0
0
0
1
1
1
1
00
01
11
100
101
指的是,任何一个字符的编码都
不是同一字符集中另一个字符的编码
的前缀。
三、前缀编码
利用赫夫曼树可以构造一种不等长的二进制编码,并且构造所得的赫夫曼编码是一种最优前缀编码,即使所传电文的总长度最短。
 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的层数是4
7)树的深度是4
8)以结点G为根的子树的深度是1
9)树的度是3
2. 答: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、1
6.答:
(1)
(2)
7.答:
先序遍历序列:A、B、D、E、F、C、G
中序遍历序列:D、F、E、B、A、G、C
后序遍历序列:F、E、D、B、G、C、A
8.答:二叉排序树如下:
9. 答:本题对应的对应的哈夫曼树如下:
各字符对应的哈夫曼编码如下:
a: 10, b: 001, c: 000, d: 11, e: 01。
二、算法设计题答案
1. 解:求二叉树结点总数的算法如下:
int CountNode(btree *t,int num)
{ if(t!=NULL)
{
num++;
num=CountNode(t->left,num);
num=CountNode(t->right,num);
}
return num;
}
求二叉树叶子结点总数的算法如下:
int CountLeaf(btree *t,int num)
{ 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)

{
if(t!=NULL)
{
p=search(t->left,x,p);
if(t->data==x)
p=t;
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+1
Delete(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 Stack
InitStack(&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 的栈顶元素。
a1
a2
an
… …
ClearStack(&S)
初始条件:栈 S 已存在。
操作结果:将 S 清为空栈。
Push(&S, e)
初始条件:栈 S 已存在。
操作结果:插入元素 e 为新的栈顶元素。
a1
a2
an
e
… …
Pop(&S, &e)
初始条件:栈 S 已存在且非空。
操作结果:删除 S 的栈顶元素,并用 e 返回其值。
a1
a2
an
an-1
… …
3.2 栈的应用举例
例一、 数制转换
例二、 括号匹配的检验
例三、 行编辑程序问题
例四、 迷宫求解
例五、 表达式求值
例六、 实现递归
例一、 数制转换
算法基于原理:
N = (N div d)×d + N mod d
例如:(1348)10 = (2504)8 ,其运算过程如下:
N N div 8 N mod 8
1348 168 4
168 21 0
21 2 5
2 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 1
1 2 2
2 2 2
3 2 1
3 3 1
3 4 4
2 4 1
2 5 1
2 6 4
1 6 3
1 5 3
1 4 4
3
$
$
$
$
$
$
$
$
求迷宫路径算法的基本思想是:
若当前位置“可通”,则纳入路径,继续前进;
若当前位置“不可通”,则后退,换方向继续探索;
若四周“均无通路”,则将当前位置从路径中删除出去。
设定当前位置的初值为入口位置;
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 b
d/e
c-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 }// b
Main的数据区
函数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移到z
3 else {
4 hanoi(n-1, x, z, y); // 将x上编号为1至n-1的
//圆盘移到y, z作辅助塔
5 move(x, n, z); // 将编号为n的圆盘从x移到z
6 hanoi(n-1, y, x, z); // 将y上编号为1至n-1的
//圆盘移到z, x作辅助塔
7 }
8 }
8 3 a b c
返址 n x y z
5 2 a c b
5 1 a b c
7 1 c a b
void 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 的空顺序栈 S
S.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;
// 否则返回ERROR
if (S.top == S.base) return ERROR;
e = *--S.top;
return OK;
}
栈顶指针
链栈

a1
an
注意: 链栈中指针的方向
an-1
ADT 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的队头元素。
a1
a2
an
… …
ClearQueue(&Q)
初始条件:队列Q已存在。
操作结果:将Q清为空队列。
EnQueue(&Q, e)
初始条件:队列Q已存在。
操作结果:插入元素e为Q的新的队尾元素。
a1
a2
an
e
… …
DeQueue(&Q, &e)
初始条件:Q为非空队列。
操作结果:删除Q的队头元素,并用e返回其值。
a1
a2
an
… …
3.5 队列类型的实现
链队列——链式映象
循环队列——顺序映象
typedef struct QNode {// 结点类型
QElemType data;
struct QNode *next;
} QNode, *QueuePtr;
链队列——链式映象
typedef struct { // 链队列类型
QueuePtr front; // 队头指针
QueuePtr rear; // 队尾指针
} LinkQueue;
a1

an

Q.front
Q.rear
Q.front
Q.rear

空队列
Status InitQueue (LinkQueue &Q) {
// 构造一个空队列Q
Q.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;否则返回ERROR
if (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];
if (f==r)
{ f=0;

r=0;
}
else
f=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 的
// 空循环队列 Q
Q.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; 否则返回ERROR
if (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。
1
1
0
0
q.front
q.rear
1
1
0
0
1
q.front
q.rear
1
1
0
2
1
q.front
q.rear
1
1
0
2
1
q.front
q.rear
1
0
0
2
1
q.front
q.rear
1
0
1
2
1
q.front
q.rear
1
0
1
2
1
q.front
q.rear
1
0
1
2
3
q.front
q.rear
1
0
1
3
3
q.front
q.rear
1
0
1
3
3
q.front
q.rear
do {
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),
使同一子集中的元素均无冲突关系,并要求划分的子集数目尽可能地少。
“划分子集”问题即为:
对前述例子而言,问题即为:
同一子集的项目为可以同时进行的项目,显然希望运动会的日程尽可能短。
可利用“过筛”的方法来解决划分子集问题。从第一个元素考虑起,凡不和第一个元素发生冲突的元素都可以和它分在同一子集中,然后再“过筛”出一批互不冲突的元素为第二个子集,依次类推,直至所有元素都进入某个子集为止。

0
1
2
3
4
5
6
7
8
0
0
1
0
0
0
1
0
0
0
1
1
0
0
0
1
1
0
1
1
2
0
0
0
0
0
1
1
0
0
3
0
0
0
0
1
0
0
0
1
4
0
1
0
1
0
0
1
0
1
5
1
1
1
0
0
0
1
0
0
6
0
0
1
0
1
1
0
0
0
7
0
1
0
0
0
0
0
0
0
8
0
1
0
1
1
0
0
0
0
012345678
14568
458
8
0 1 0 0 0 1 0 0 0
0 1 0 0 0 2 1 0 0
0 1 0 0 1 2 1 0 1
0 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;
else
Pop(S);
}
if(StackEmpty(S))
return 1;
else
return 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,1
a0,0
a0,2
a1,0
a1,1
a1,2
a0,1
a0,0
a0,2
a1,0
a1,1
a1,2
L
L
推广到一般情况,可得到 n 维数组数据元素存储位置的映象关系
称为 n 维数组的映象函数。数组元素
的存储位置是其下标的线性函数
其中 cn = L,ci-1 = bi ×ci , 1 < i n。
LOC(j1, j2, ..., jn ) = LOC(0,0,...,0) + ∑ ci ji
i
=1
n
假设 m 行 n 列的矩阵含 t 个非零元素,则称
为稀疏因子
通常认为 0.05 的矩阵为稀疏矩阵
5.3 稀疏矩阵的压缩存储
何谓稀疏矩阵?
以常规方法,即以二维数组表示
高阶的稀疏矩阵时产生的问题:
1) 零值元素占了很大空间;
2) 计算中进行了很多和零值的运算,
遇除法,还需判别除数是否为零;
1) 尽可能少存或不存零值元素;
解决问题的原则:
2) 尽可能减少没有实际意义的运算;
3) 操作方便; 即:
能尽可能快地找到
与下标值 (i, j) 对应的元素;
能尽可能快地找到
同一行或同一列的非零值元;
1) 特殊矩阵
非零元在矩阵中的分布有一定规则
例如: 三角矩阵
对角矩阵
2) 随机稀疏矩阵
非零元在矩阵中随机出现
有两类稀疏矩阵:
随机稀疏矩阵的压缩存储方法:
一、三元组顺序表
二、行逻辑联接的顺序表
三、 十字链表
#define MAXSIZE 12500
typedef 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 14
1 5 -5
2 2 -7
3 1 36
3 4 28
2 1 14
5 1 -5
2 2 -7
1 3 36
4 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) { }
} // if
return 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 500
typedef 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
} // if
return OK;
} // MultSMatrix
ctemp[] = 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 5
0 -1 0 0
2 0 0 0
1
1
3
1
4
5
2
2
-1
3
1
2
^
^
^
^
^
^
^
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))
D
E
F
a
( )
d
( )
b
c
e
广义表 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 tp
tag=0 data
1) 表头、表尾分析法:
构造存储结构的两种分析方法:
若表头为原子,则为
空表 ls=NIL
非空表 ls
tag=1
指向表头的指针
指向表尾的指针
tag=0 data
否则,依次类推。
L=(a, (x, y), ((x)))
L ((x, y), ((x))) (((x)))
1 1
a (x, y)
L = ( a, ( x, y ), ( ( x ) ) )
a
( x, y )
( )
1
L
L = ( )
0 a
1
1
1
1
1
0 x



( )
x
2) 子表分析法:
若子表为原子,则为
空表 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)的结果是:
1
2
3 3
4 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;
};
} *GList
tag=1
hp tp
ptr
表结点
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;
} // GlistDepth
if (!L) return 1;
if (L->tag == ATOM) return 0;
1
1
1
L


for (max=0, pp=L; pp; pp=pp->ptr.tp){
dep = GlistDepth(pp->ptr.hp);
if (dep > max) max = dep;
}
例如:
pp
pp->ptr.hp
pp
pp
pp->ptr.hp
pp->ptr.hp
L = ( a, ( x, y ), ( ( x ) ) )
a
( x, y )
( )
1
L
L = ( )
0 a
1
1
1
1
1
0 x



( )
x
后置递归的设计思想为:
递归的终结状态是,当前的问题可以直接求解,对原问题而言,则是已走到了求解的最后一步。
链表是可以如此求解的一个典型例子。
例如:编写“删除单链表中所有值为x 的数据元素”的算法。
1) 单链表是一种顺序结构,必须从第一个结点起,逐个检查每个结点的数据元素;
分析:
2) 从另一角度看,链表又是一个递归结构,若 L 是线性链表 (a1, a2, , an) 的头指针,则 L->next是线性链表 (a2, , an)的头指针。
a1
a2
a3
an


L
例如:
a1
a2
a3
an

L
a1
a2
a3
an

L
已知下列链表
1) “a1=x”, 则 L 仍为删除 x 后的链表头指针
2) “a1≠x”, 则余下问题是考虑以 L->next 为头指针的链表


a1
L->next
L->next=p->next
p=L->next
void 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); // 递归处理剩余表项
1
L
0 x
1
p
L
head
if (head->tag == LIST) //该项为广义表
Delete_GL(head, x);
Delete_GL(L->ptr.tp, x);
// 递归处理剩余表项
1
L
0 a
1
1
head
L->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!的递归函数的递归树已退化为一个单枝树;而计算斐波那契递归函数的递归树中有很多重复出现的结点。
n
n-1
1
0
。。。
F5
F4
F3
F3
F2
F2
F1
F1
F0
F1
F0
。。。
如: 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 { }
} // else
return OK;
} // CopyGList
分别复制表头和表尾
CopyGList(T->ptr.hp, L->ptr.hp);
// 复制求得表头T->ptr.hp的一个副本L->ptr.hp
CopyGList(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= i
if (!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;
}
} // B
5. 可以用递归方程来表述递归函数的
时间性能。
例如: 假设解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( )的元素的位序。若这样的元素不存在则返回值为0
ListTraverse(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 String
StrAssign (&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 ) < 0
StrCompare( cat , case ) > 0
StrLength (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
bca
bca
bca
StrInsert (&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 串
i
pos
n-m+1
算法的基本思想为:
0
int Index (String S, String T, int pos) {
// T为非空串。若主串S中第pos个字符之后存在与 T相等的子串,
// 则返回第一个这样的子串在S中的 位置,否则返回0
if (pos > 0) {
} // if
return 0; // S中不存在与T相等的子串
} // Index
n = StrLength(S); m = StrLength(T); i = pos;
while ( i <= n-m+1) {
} // while
SubString (sub, S, i, m);
if (StrCompare(sub,T) != 0) ++i ;
else return i ;
又如串的置换函数:
S 串
T 串
V 串
V 串
pos
pos
sub
i
news 串
sub
= i+m
pos
n-pos+1
void 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
}//while
SubString(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] == MAXSTRLEN
uncut = FALSE; }
typedef struct {
char *ch;
// 若是非空串,则按串实用长度分配
//存储区,否则 ch 为NULL
int 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;
} // Concat
Status 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 个字符之前插入串 T
slen=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]; // 暂存 S
if (tlen>0) // T 非空,则为S重新分配空间并插入T
{ }
} // StrInsert _HSq
……
S.ch = new char[slen + tlen ];
// 为 S 重新分配空间
for ( i=0, k=0; i// 保留插入位置之前的子串
for ( i=0; i// 插入T
for ( 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中的 位置,否则返回0
if (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
} // if
return 0; // S中不存在与T相等的子串
} // Index
下面讨论以定长顺序结构
表示串时的几种算法。
一、简单算法
三、KMP(D.E.Knuth,V.R.Pratt,
J.H.Morris) 算法
二、首尾匹配算法
S 串
T 串
T 串
i
pos
n-m+1
S 串
T 串
T 串
i
j
j>m
i
j
i
j
i
j
i>n
j
T 串
一、简单算法
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]= 01234
nextval[j]=00004
void 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_nextval
1. 熟悉串的七种基本操作的定义,并能利用这些基本操作来实现串的其它各种操作的方法。
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 =
i
i
int 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;
} //location
i<=L.length
ST.elem
i
ST.elem
i
60
i
kval = 64
kval = 60
i
64
int Search_Seq(SSTable ST,
KeyType kval) {
// 在顺序表ST中顺序查找其关键字等于
// key的数据元素。若找到,则函数值为
// 该元素在表中的位置,否则为0。
ST.elem[0].key = kval; // 设置“哨兵”
for (i=ST.length; --i);
// 从后往前找
return i; // 找不到时,i为0
} // Search_Seq
ST.elem[i].key!=kval;
定义: 查找算法的平均查找长度
(Average Search Length)
为确定记录在查找表中的位置,需和给定值
进行比较的关键字个数的期望值
其中: n 为表长,Pi 为查找表中第i个记录的概率,
且 Ci为找到该记录时,曾和给定值
比较过的关键字的个数
分析顺序查找的时间性能。
在等概率查找的情况下,
顺序表查找的平均查找长度为:
对顺序表而言,Ci = n-i+1
ASL = nP1 +(n-1)P2 + +2Pn-1+Pn
若查找概率无法事先测定,则查找过程采取的改进办法是,在每次查找之后,将刚刚查找到的记录直接移至表尾的位置上。
在不等概率查找的情况下,ASLss 在
Pn≥Pn-1≥···≥P2≥P1
时取极小值
上述顺序查找表的查找算法简单,
但平均查找长度较大,特别不适用于表长较大的查找表。
二、有序查找表
若以有序表表示静态查找表,则查找过程可以基于“折半”进行。
ST.elem
ST.length
例如: key = 64 的查找过程如下
low
high
mid
low
mid
high
mid
low 指示查找区间的下界;
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
分析折半查找的平均查找长度
6
3
9
1
4
2
5
7
8
10
11
判定树
1
2
2
3
3
3
3
4
4
4
4
假设 n=2h-1 并且查找概率相等

在n>50时,可得近似结果
一般情况下,表长为 n 的折半查找的判定树的深度和含有 n 个结点的完全二叉树的深度相同。
关键字: A B C D E
Pi: 0.2 0.3 0.05 0.3 0.15
Ci: 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,
则推导可得
0
2
3
8
11
15
18
23
例如:
l
h
21
18
12
4
3
10
18
h
9
6
0
8
E
C
2
1
A
h
5
3
l
h
G
3
0
1
3
E
C
G
A
B
D
F
所得次优二叉树如下所示:
查找比较“总次数”
= 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
和折半查找相比较
D
B
A
C
F
E
G
Status 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 的数据元素含有权域 weight
if (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)若它的右子树不空,则右子树上
所有结点的值均大于根结点的值;
50
30
80
20
90
10
85
40
35
25
23
88
例如:
是二叉排序树。
66

通常,取二叉链表作为
二叉排序树的存储结构
typedef struct BiTNode { // 结点结构
struct BiTNode *lchild, *rchild;
// 左右孩子指针
} BiTNode, *BiTree;
TElemType data;
2.二叉排序树的查找算法:
1)若给定值等于根结点的关键字,则查找成功;
2)若给定值小于根结点的关键字,则继续在左子树上进行查找;
3)若给定值大于根结点的关键字,则继续在右子树上进行查找。
否则
若二叉排序树为空,则查找不成功;
50
30
80
20
90
85
40
35
88
32
例如:
二叉排序树
查找关键字
== 50 ,
50
50
35 ,
50
30
40
35
50
90 ,
50
80
90
95 ,
从上述查找过程可见,
在查找过程中,生成了一条查找路径:
从根结点出发,沿着左分支或右分支逐层向下直至关键字等于给定值的结点;
或者
从根结点出发,沿着左分支或右分支逐层向下直至指针指向空树为止。
——查找成功
——查找不成功
算法描述如下:
Status SearchBST (BiTree T, KeyType key,
BiTree f, BiTree &p ) {
// 在根指针 T 所指二叉排序树中递归地查找其
// 关键字等于 key 的数据元素,若查找成功,
// 则返回指针 p 指向该数据元素的结点,并返回
// 函数值为 TRUE;
} // SearchBST
… … … …
否则表明查找不成功,返回
// 指针 p 指向查找路径上访问的最后一个结点,
// 并返回函数值为FALSE, 指针 f 指向当前访问
// 的结点的双亲,其初始调用值为NULL
if (!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 );
// 在右子树中继续查找
f
T
设 key = 48
f
T
f
T
22
p
f
T
f
T
T
T
T
f
f
f
p
30
20
10
40
35
25
23
根据动态查找表的定义,“插入”操作在查找不成功时才进行;
3.二叉排序树的插入算法
若二叉排序树为空树,则新插入的结点为新的根结点;否则,新插入的结点必为一个新的叶子结点,其插入位置由查找过程得到。
Status Insert BST(BiTree &T, ElemType e )
{
// 当二叉排序树中不存在关键字等于 e.key 的
// 数据元素时,插入元素值为 e 的结点,并返
// 回 TRUE; 否则,不进行插入并返回FALSE
if (!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.二叉排序树的删除算法
可分三种情况讨论:
和插入相反,删除在查找成功之后进行,并且要求在删除二叉排序树上某个结点之后,仍然保持二叉排序树的特性。
50
30
80
20
90
85
40
35
88
32
(1)被删除的结点是叶子结点
例如:
被删关键字 = 20
88
其双亲结点中相应指针域的值改为“空”
50
30
80
20
90
85
40
35
88
32
(2)被删除的结点只有左子树
或者只有右子树
其双亲结点的相应指针域的值改为 “指向被删除结点的左子树或右子树”。
被删关键字 = 40
80
50
30
80
20
90
85
40
35
88
32
(3)被删除的结点既有左子树,也有右子树
40
40
以其前驱替代之,然后再删除该前驱结点
被删结点
前驱结点
被删关键字 = 50
Status DeleteBST (BiTree &T, KeyType key ) {
// 若二叉排序树 T 中存在其关键字等于 key 的
// 数据元素,则删除该数据元素结点,并返回
// 函数值 TRUE,否则返回函数值 FALSE
if (!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);
p
q
q
// 左子树为空树只需重接它的右子树
q = p; p = p->rchild; delete(q);
p
p
q
q
q = 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);
p
q
s
5.查找性能的分析
对于每一棵特定的二叉排序树,均可按照平均查找长度的定义来求它的 ASL 值,显然,由值相同的 n 个关键字,构造所得的不同形态的各棵二叉排序树的平均查找长 度的值不同,甚至可能差别很大。
由关键字序列 3,1,2,5,4构造而得的二叉排序树
由关键字序列 1,2,3,4,5构造而得的二叉排序树,
例如:
2
1
3
4
5
3
5
4
1
2
ASL =(1+2+3+4+5)/ 5
= 3
ASL =(1+2+3+2+3)/ 5
= 2.2
下面讨论平均情况:
不失一般性,假设长度为 n 的序列中有 k 个关键字小于第一个关键字,则必有 n-k-1 个关键字大于第一个关键字,由它构造的二叉排序树
n-k-1
k
的平均查找长度是 n 和 k 的函数
P(n, k) ( 0 k n-1 )
假设 n 个关键字可能出现的 n! 种排列的可能性相同,则含 n 个关键字的二叉排序树的平均查找长度
在等概率查找的情况下,
由此
可类似于解差分方程,此递归方程有解:
二、二叉平衡树
何谓“二叉平衡树”?
二叉平衡树的查找性能分析
如何构造“二叉平衡树”
二叉平衡树是二叉查找树的另一种形式,其特点为:
树中每个结点的左、右子树深度之差的绝对值不大于1 。
例如:
5
4
8
2
5
4
8
2
1
是平衡树
不是平衡树
构造二叉平衡(查找)树的方法是:
在插入过程中,采用平衡旋转技术。
例如:依次插入的关键字为5, 4, 2, 8, 6, 9
5
4
2
4
2
5
8
6
6
5
8
4
2
向右旋转
一次
先向右旋转
再向左旋转
4
2
6
5
8
9
4
2
6
8
9
5
向左旋转一次
继续插入关键字 9
在平衡树上进行查找的过程和二叉排序树相同,因此,查找过程中和给定值进行比较的关键字的个数不超过平衡 树的深度。
平衡树的查找性能分析:
问:含 n 个关键字的二叉平衡树可能达到的最大深度是多少?
n = 0
空树
最大深度为 0
n = 1
最大深度为 1
n = 2
最大深度为 2
n = 4
最大深度为 3
n = 7
最大深度为 4
先看几个具体情况:
反过来问,深度为 h 的二叉平衡树中所含结点的最小值 Nh 是多少?
h = 0
N0 = 0
h = 1
h = 2
h = 3
一般情况下,
N1 = 1
N2 = 2
N3 = 4
Nh = 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 80
80
30
50
和插入的考虑相反,首先必须找到待删关键字所在结点,并且要求删除之后,结点中关键字的个数不能小于 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-1
H-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 96
15 50
62 78 96
71 78
84 89 96
56 62
20 26 43 50
3 8 15
sq
root
五、键 树
1. 键树的结构特点
2. .双链树
3. Trie树
1. 键树的结构特点:
※ 关键字中的各个符号分布在从根结点到叶的路径上,叶结点内的符号为“结束”的标志符。因此,键树的深度和关键字集合的大小无关;
※ 键树被约定为是一棵有序树,即同一层中兄弟结点之间依所含符号自左至右有序,并约定结束符‘$’小于任何其它符号。
H
A
D
$
S
$
V
E
$
E
$
R
$
E
$
I
G
H
$
S
$
例如:
表示关键字集合
{HAD, HAS, HAVE, HE, HER, HERE, HIGH, HIS }
2. 双链树
— 以二叉链表作存储结构实现的键树
typedef enum { LEAF, BRANCH }NodeKind;
// 两种结点:{叶子 和 分支}
结点结构:
first symbol next
分支结点
infoptr symbol next
叶子结点
指向孩子结点
的指针
指向兄弟结点
的指针
指向记录
的指针

H

A
D
$
HAD
E
$
R
$
$
E
S
$
G
H
$
I








HE
HER
HERE
HIGH
HIS

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) … … 26
8(H)
4(D) 19(S) 22(V) 0 18(R) 7(G) 19
0 5(E)
T
HAD
HAS
HAVE
HE
HER
HERE
HIGH
HIS









叶子结点
分支结点
指向记录
的指针
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
Chen
Zhao
Qian
Sun
Li
Wu
Han
Ye
Dei
问题: 若添加关键字 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 + b
1. 直接定址法
此法仅适合于:
地址集合的大小 = = 关键字集合的大小
此方法仅适合于:
能预先估计出全体关键字的每一位上各种数字出现的频度。
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 m
i=1, 2, …, s
1. 开放定址法
对增量 di 有三种取法:
1) 线性探测再散列 di = c i 最简单的情况 c=1
2) 平方探测再散列 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 )
19
01
23
14
55
68
19
01
23
14
68
若采用线性探测再散列处理冲突
若采用二次探测再散列处理冲突
11
82
36
55
11
82
36
1 1 2 1 3 6 2 5 1
H2(key) 是另设定的一个哈希函数,它的函数值应和 m 互为素数。
若 m 为素数,则 H2(key) 可以是 1 至 m-1 之间的任意数;
若 m 为 2 的幂次,则
H2(key) 应是 1 至 m-1 之间的任意奇数。
例如,当 m=11时,
可设 H2(key)=(3 key) MOD 10+1
19
01
23
14
55
68
11
82
36
2 1 1 1 2 1 2 1 3
将所有哈希地址相同的记录
都链接在同一链表中。
2. 链地址法
0
1
2
3
4
5
6

11

19
82
68

55

14
36

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的记录
} // SearchHash
p = Hash(K); // 求得哈希地址
while ( H.elem[p].key != NULLKEY &&
!EQ(K, H.elem[p].key))
collision(p, ++c); // 求得下一探查地址 p
if (EQ(K, H.elem[p].key)) return SUCCESS;
// 查找成功,返回待查数据元素位置 p
else return UNSUCCESS; // 查找不成功
Status InsertHash (HashTable &H, Elemtype e){
} // InsertHash
c = 0;
if ( HashSearch ( H, e.key, p, c ) == SUCCESS )
return DUPLICATE;
// 表中已有与 e 有相同关键字的元素
else
H.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/9
14/9
13/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 =9
H(12) = 12 % 13 =12
H(180) = 180 % 13 =11
H(138) =138 % 13 =8
H(92) = 92 % 13 =1
H(67) = 67 % 13 =2
H(94) = 94% 13 =3
H(134) = 134% 13 =4
H(252) = 252 % 13 =5
H(6) = 6% 13 =6
H(70) = 70 % 13 =5
H(323) = 323% 13 =11
H(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);
}
else
return(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)
{
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;
(*num)++;
return TRUE;}
P18【算法2.2 顺序表的删除】
int Delete(Elemtype List[],int *num,int i)
{
int j;
if(i<0||i>*num)
{printf(“Error!”); return FALSE; }
for(j=i+1;j<=*num;j++)
List[j-1]=List[j];
(*num)--;
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);
Initiate(Lc);
While (i<=La_length&&j<=Lb_length)
{ a=get(La,i);b=get(Lb,j);
if(aelse {insert(Lc,++k,b);++j;}
}
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)
{
slnodetype *p,*q,*s;
int j=0;
p=h;
while(p!=NULL&&jnext;j++; }
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;
else q->next=p;
q=p;
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;}
s->next=p;q->next=s;
}
P24【算法2.7 单链表的删除】
int Delet(slnodetype *h,int i)
{
slnodetype *p,*s;
int j;
p=h;j=0;
while(p->next!=NULL&&j{ p=p->next;j=j+1; }
if(j!=i-1)
{printf(“Error!”);
return FALSE; }
s=p->next;
p->next=p->next->next;
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)
{
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;
r=qa;Ch=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++;}
else {r->next=qb;r=qb;qb++;}
}
if(qa==NULL) r->next=qb;
else r->next=qa;
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)
{
if ((s=(sqstack*)malloc(sizeof(sqstack)))= =NULL) return FALSE;
s->top= -1;
return TRUE;
}
P39【算法3.2 入栈操作】
int push(sqstack *s, Elemtype x)
{
if(s->top>=MAXNUM-1) return FALSE;
s->top++;
s->stack[s->top]=x;
return TRUE;
}
P39【算法3.3 出栈操作】
Elemtype pop(sqstack *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)
{
if(s->top<0) return NULL;
return (s->stack[s->top]);
}
P40【算法3.5 判栈空操作】
int Empty(sqstack *s)
{
if(s->top<0) return TRUE;
return FALSE;
}
P40【算法3.6 栈置空操作】
void setEmpty(sqstack *s)
{
s->top= -1;
}
P40 C语言定义的这种两栈共享邻接空间的结构如下:
Typedef struct {
Elemtype stack[MAXNUM];
int lefttop;
int righttop;
} dupsqstack;
P41【算法3.7 共享栈的初始化】
int initDupStack(dupsqstack *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)
{
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)
{
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)
{
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)
{
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)
{
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)
{
if(s->top>=MAXNUM-1) return FALSE;
s->top++;
s->stack[s->top]=x;
return TRUE;
}
char pop(sqstack *s)
{
char x;
if(s->top<0) return NULL;
x=s->stack[s->top];
s->top--;
return x;
}
char gettop(sqstack *s)
{
if(s->top<0) return NULL;
return (s->stack[s->top]);
}
char precede(char x1,char 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;
}
else
switch (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)
{
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)
{
if(q->rear>=MAXNUM-1) return FALSE;
q->rear++;
q->queue[q->rear]=x;
return TRUE;
}
P52【算法3.17 顺序队列的出队列操作】
Elemtype delete(sqqueue *q)
{
Elemtype x;
if(q->rear= =q->front) return NULL;
x=q->queue[++q->front];
return x;
}
P52【算法3.18 顺序队列的取头元素操作】
Elemtype getHead(sqqueue *q)
{
if(q->rear= =q->front) return NULL;
return (q->queue[s->front+1]);
}
P52【算法3.19 顺序队列的非空判断操作】
int Empty(sqqueue *q)
{
if (q->rear= =q->front) return TRUE;
return FALSE;
}
P53【算法3.20 顺序队列的求长度操作】
int length(sqqueue *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)
{
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)
{
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)
{
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)
{
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)
{
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)
{
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 80
typedef 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);
if (n>k) s2->length=k;
else s2->length=n;
s2->str=s1.str+m;
k=strlen(s1.str+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;
struct linknode next;
} k; }
P81 (1)按照A的列序进行转置
#define maxsize 100
struct 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;
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 100
struct 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 100
struct 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;

q=cp[i];
while ((q->rptr!=cp[i]) &&( q->rptr->jq=q->rptr;
p->rptr=q->rptr;
q->rptr=p;

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 稀疏矩阵十字链表相加算法如下:

#include
#define maxsize 100
struct 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;

q=cp[i];
while ((q->rptr!=cp[i]) &&( q->rptr->jq=q->rptr;
p->rptr=q->rptr;
q->rptr=p;

q=cp[j];
while((q->cptr!=cp[j]) &&( q->cptr->iq=q->cptr;
p->cptr=q->cptr;
q->cptr=p;
}
return hm;
}

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( );
hb=creatlindmat( );
printf("A:\n");
print(ha);printf("\n");
printf("B:\n");
print(hb);printf("\n");
hc=matadd(ha,hb);
printf("A+B:\n");
print(hc);printf("\n");
}
P94 数据类型描述如下:
#define elemtype char
struct 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(“#”);
else
print(LS->ds.slink);
}
else
printf(“%c ”,LS->ds.data);
if(LS->atom==0)
printf(“)”);
if(LS->link!=NULL)
{
printf(“;”);
print(LS->link);
}
}
P98 该算法的时间复杂度为O(n)。整个完整程序如下:
#include
#define elemtype char
struct 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("#");
else
print(LS->ds.slink);
}
else
printf("%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)

{ if (t!=NULL)
{printf(“%d ”,t->data);
preorder(t->lch);
preorder(t->rch);
}
}
P113 C语言的中序遍历算法:
void inorder(tnodetype *t)

{
if(t!=NULL)
{inorder(t->lch);
printf(“%d ”,t->data);
inorder(t->rch);
}
}
P113 C语言的后序遍历算法:
void postorder(tnodetype *t)

{
if(t!=NULL)
{ postorder(t->lch);
postorder(t->rch);
printf(“%d ”,t->data);
}
}
P114 如果引入队列作为辅助存储工具,按层次遍历二叉树的算法可描述如下:
void levelorder(tnodetype *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)
{ rear++;
q [rear]=t->lch;
}
if (t->rch!=NULL)
{ 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)

{ 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;
}
if (pr!=NULL)
{ if (pr->rch!=NULL)
pr->rtag=0;
else { pr->rtag=1;
pr->rch=p;
}
}
pr=p;
inorderxs (t->rch);
}
}
P118 在中根线索树上检索某结点的前驱结点的算法描述如下:
struct nodexs * inpre (struct nodexs *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)

{ 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)

{ if (t==NULL)
t=s;
else if (s->data < t->data)
insertbst(t->lch,s);
else
insertbst(t->rch,s);
}
P121 二叉排序树结点删除算法的C语言描述如下:
void delnode(bt,f,p)


{ fag=0;
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;
}
if (q=p)
q->lch=s->lch;
else q->rch=s->lch;
p->data=s->data;
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);
ga ->arcs[i][j]=w;
ga - >arcs[j][i]=w;
}
}
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);
s=malloc(sizeof(edgenode));
s-> adjvex=j;
s- - >next:=ga[i].Link;
ga[i].1ink=s;
s=malloc(size0f(edgende));
s ->adjvex=i;
s ->next=ga[j].1ink;
ga[j].1ink=s;
}
}
P139 分别以邻接矩阵和邻接表作为图的存储结构给出具体算法,算法中g、g1和visited为全程量,visited的各分量初始值均为FALSE。
int visited[n]
Graph g;
DFS(i)
int i;
{ int j;
printf(“node:%c\n” , g.vexs[i]);
Visited[i]=TRUE;
for (j=0;jif((g.arcs[i][j]==1) &&(! visited[j]))
DFS(j);
}
vexnode gl[n]
DFSL(i)
int i;
{ int j;
edgenode * p;
printf(“node:%C\n” ,g1[i].vertex);
vistited[i]=TRUE;
p=g1[i].1ink;
while(p !=NULL)
{
if(! Vistited[p ->adjvex])
DFSL(p - >adjvex);
p=p - >next;
}
}
P142 以邻接矩阵和邻接表作为图的存储结构,分别给出宽度优先搜索算法。
BFS(k) /*从vk+l出发宽度优先搜索图g,g用邻接矩阵表示,visited为访问标志向量*/
int k;
{ int i,j;
SETNULL(Q);
printf(“%c\n”,g.vexs[k]);
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]);
visited[j]=TRUE;
ENQUEUE(Q,j);
}
}
}
BFSL(k)
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
while(p !=NULL)
{ if( ! visited[p - >adjvex])
{ printf{“%c\n” , g1[p - >adjvex].vertex};
visited[p - >adjvex]=TRUE;
ENQUEUE(Q,p - >adjvex);
}
p=p - >next;
}
}
}
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;
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,
v=T[kl.Endvex];
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()
{int j , k , m , v , min , max=l0000;
float d;
edge e;
for(j=1;j{T[j-1].formvex=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;
}
}
e=T[m];T[m]=T[k];T[k]=e;
v=T[k].endvex ;
for(j=k+1;j{d=dist[v-1][T[j].endvex-1];
if(d{T[j].1ength=d;
T[j].fromvex=v;
}
}
}
P151 Kruskl算法的粗略描述:
T=(V,φ);
While(T中所含边数{从E中选取当前最短边(u,v);
从E中删去边(u,v);
if((u,v)并入T之后不产生回路,将边(u,v)并入T中;
}
P153 迪杰斯特拉算法实现。算法描述如下:
#define max 32767
void dijkstra (float cost[][n],int v)

{ v1=v-1;
for (i=0;i{ dist[i]=cost[v1][i];
if (dist[i]pre[i]=v;
else pre[i]=0;
}
pre[v1]=0;
for (i=0;is[i]=0;
s[v1]=1;
for (i=0;i{ min=max;
for (j=0;jif (!s[j] && (dist[j]{ min=dist[j];
k=j;
}
s[k]=1;
for (j=0;jif (!s[j]&&(dist[j]>dist[k]+cost[k][j]))
{ dist[j]=dist[k]+cost[k][j];
pre[j]=k+1;
}
}
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,…,n
P155 弗洛伊德算法实现。算法描述如下:
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
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];
if (next==0)
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);
}
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;
}
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)

{ for(i=2;i<=n;i++)
{ r[0]=r[i];
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)

{ 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];
int d[t];
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]};
j=i-h;
while(temp.key{R[j+h]=R[j]};
j=j-h;
}
R[j+h]=temp;
}
k++;
} while (h!=1);
}
P180 选择排序算法如下:
void selectsort(struct node r[n+1],int 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)

{ x=r[i];j=2*i;
while (j<=m)
{ if ((jj=j+1;
if (x.key{ r[i]=r[j];
i=j;
j=2*i;
}
else j=m+1
}
r[i]=x;
}
P183 堆排序算法如下:
void heapsort (struct node r[n+1],int m)

{ 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)
}
}
P185 一次划分及其排序的算法。
int partition(r,1,h)
rectype R[ ];
int 1,h;
{int i,j;
rectype temp;
i=1;j=h temp=R[i];
Do{
While((R[j].key>=temp.key) && (ij--;
if(iwhile((R[i].key<=temp.key) && (ii++;
if(i}
quicksort(R,s1,t1)
rectype R[ ];
int s1,t1;
{int i;
if (s1{i= partition (R,s1,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);
else
for(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));
printf(“\n %6d%6d%6d”,x,y,z);
display();
x=5; y=6; z=7;
sa=sumx(x, y, z);
printf(“\n “ sum=%d”,sa);
printf(“\n %6d%6d%6d”,x,y,z);
display();
}
运行结果:
----------------------
sum= 6
2 2 2
----------------------
sum=48
15 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);
printf(“\n %6d%6d”, x ,y);
}
viod swap( int *a, int *b)
{ int c;
c=*a; *a = *b; *b=c;
}
运行结果:
800
800 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发生了变化。由此可以看出C语言的传址调用比较复杂。不如PASCAL的变量参数简便,也不如C++的引用调用方便。
三、 结构体及运用
数据结构课程所研究的问题均运用到“结构体”。在C语言中结构体的定义、输入/输出是数据结构程序设计的重要语法基础。定义结构体的一般格式:
struct 结构体类型名
{ 类型名1 变量名1; //数据子域
类型名2 变量名2;……
类型名n 变量名n;
};
其中struct是保留字。结构体类型名由用户自己命名。在使用时必须声明一个具体的结构体类型的变量,声明创建一个结构体变量的方法是:
struct 结构体类型名 结构体变量名;
例如: struct ElemType
{ int num; char name[10];
} ;
struct ElemType x;
另外有一种方法使用typedef 语句定义结构体,在声明结构体变量时可以不写struct,使得书写更加简便。例如:
typedef struct
{ int num;
char name[10];
} ElemType;
ElemType就是一个新的类型名,并且是结构体类型名。声明变量x的语句是:
ElemType x;
一个结构体中可以包含多个数据子域。数据子域的类型名一般指基本数据类型(int char 等),也可是已经定义的另一结构体名。数据子域变量名可以是简单变量,也可以是数组。它们也可以称为结构体的数据成员。
通过“结构体变量名.数据子域” 可以访问数据子域。
例1.6 设计Student结构体,在主程序中运用。
#include
#include
typedef struct
{ long num;
int x;
char name[10];
} Student;
void main( )
{ Student s1;
s1.num=1001 ;
s1. x=83;
strcpy( s1.name, “ 李 明”);
printf( “\n 姓名: %s”, s1.name);
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=( Student *)malloc(sizeof( Student));
p->num=101; p->x=83; strcpy( p->name, “李 明 ”);
printf(“\n %10s%6d%6d”,p->name,p->num,p->x);
}
设计一个一维数组,每个数组元素是Student结构体类型,通过以下语句段可以说明结构体数组的一般用法。可以通过“结构体数组名[下标].数据子域”访问数据域。
{ Student a[5];
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);
}

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;
}

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);
}
三、实习题
首先将上面源程序输入计算机,进行调试。运行程序,输入下列两个复数的实部域虚部,记录两个复数相加的输出结果。 原始数据: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;
}
}while(k!=6);
printf("\n 再见!");
printf(“\n 打回车键,返回。“); ch=getch();
}

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]));
}
}

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();
}

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++;
}
}

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);
}

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);
}
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;
}
printf("\n ----------------");
}while(k>=1 && k<5);
printf("\n 再见!");
printf(“\n 打回车键,返回。“); ch=getch();
}

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)
{ 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);
}

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();
}

void insert_L(LNode *L,int i, ElemType e)
{ LNode *s,*p,*q; int j;
p=L;
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;
}
}

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);
}
}

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);
}
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);
}

JOS *creat()
{ int i=0, mi;
JOS *new, *pre, *h;
h=(JOS *)malloc(sizeof(JOS));
h->link=h;
pre=h;
printf(“\n 个人密码 = ”); scanf(“%d”,&mi);
while( mi != -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);
}
pre->link= h->link; free(h);
h=pre->link; return(h);
}

void outs(JOS *h, int m)
{ int i; JOS *q=h, p;
printf(“\n “);
while (q->link!=q)
{ for(i=1;ilink;}
printf(“%6d %6d ”,q->num,q->sm);
p->link=q->link; free(q);
q=p->link;
}
printf(“%6d %6d \n”,q->num,q->sm);
free(q);
}
本程序用了不带头结点的循环链表,也可以加上头结点,对于本题有头结点会使操作麻烦,(同学们可以试一试,加进头结点如何实现?)并不是任何时候有头结点都能使程序操作简化,要根据实际情况,决定用是否使用头结点。
感兴趣的同学可以设计一个程序实现约瑟夫环的方法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);
}
printf("\n ----------------");
}while(k>=1 && k<3);
printf("\n 再见!")
printf(“\n 打回车键,返回。“); ch=getch();
}


void out_s(SqStack s)
{ char ch; int i;
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();
}

void push(SqStack *s,ElemType e)
{ if(s->top==MAXSIZE-1)printf(“\n Sstack is Overflow!”);
else{ s->top++ ;
s->a[s->top]=e;
}
}

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);
}
循环队列(即队列的顺序存储结构)实现。
#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);
}
printf("\n ----------------");
}while(k>=1 && k<3);
printf("\n 再见!");
printf(“\n 打回车键,返回。“); ch=getch();
}


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();
}
/ * 进队函数 */
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;
}
}

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);
}
队列的链表储结构及实现。
#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);
}
printf("\n ----------------");
}while(k>=1 && k<3);
printf("\n 再见!");
printf(“\n 打回车键,返回。“); ch=getch();
}

void init_Q(L_Queue *Q)
{ QNode *p ;
p=(QNode *)malloc(sizeof(QNode));
p->next=NULL;
Q->fornt=p; Q->rear=p;
}

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();
}
/ * 进队函数 */
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;
}

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);
}
三、实习题
写一个程序,将输入的十进制数据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);
}

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);
}
}


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);
}
三、实习题
将上述实例打入计算机,调试运行。
字符串常量的提供有多种手段。可以在定义串类型同时初始化串值:
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);
}

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];
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;

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;

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;
}
}
三、实习题
编写用“三元组表”存储(参见教科书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;
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;
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);
}
printf("\n ----------------");
}while(k>=1 && k<5);
printf("\n 再见!");
printf(“\n 打回车键,返回。“); ch=getch();
}

BiTNode *creat_bt1()
{ BiTNode *t,*p,*v[20]; int i; Etype e;

printf("\n i,data= "); scanf("%d%d",&i,&e);
while(i!=0 && e!=0)
{ p=(BiTNode *)malloc(sizeof(BiTNode));
p->data=e; p->lch=NULL; p->rch=NULL;
v[i]=p;
if (i==1) t=p;
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);
}

BiTNode *creat_bt2()
{ BiTNode *t;
printf("\n data="); scanf("%d",&e);
if(e==0) t=NULL;
else { t=(BiTNode *)malloc(sizeof(BiTNode));
t->data=e;
t->lch=creat_bt2();
t->rch=creat_bt2();
}
return(t);
}

void inorder(BiTNode *p)
{ if (p) { inorder(p->lch);
printf("%3d",p->data);
inorder(p->rch);
}
}


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);
}
} 。
三、实习题
建立一棵二叉树,要求用先序非递归方法遍历二叉树。
建立一棵二叉树,要求用“按层遍历”的方法遍历二叉树”的函数。
给定一组值,建立一棵二叉树,求二叉数的树深。
哈夫曼树问题。
利用哈夫曼编码进行通讯可以大大提高信道利用率,缩短信息传输时间,降低传输成本,但是,这要求在发送端通过一个编码系统对待传数据进行预先编码;在接受端将传来的数据进行解码(复原)对于双工信道(即可以双向传输的信道),每端都要有一个完整的编/译码系统。试为这样的信息收发站写一个哈夫曼的编译码系统。
[基本要求]:
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 20
typedef int VexType;
typedef VexType Mgraph[MAX][MAX];

void creat_mg(Mgraph G);
void out_mg(Mgraph G);
Mgraph G1;
int n,e,v0;

main()
{ creat_mg(G1); out_mg(G1);
}

void creat_mg(Mgraph G)
{ int i,j,k;
printf(“\n n,e= ”); scanf(“%d%d”, &n,&e);
for(i=1; i<=n;i++)
for(j=1;j<=n;j++) G[i][j]=0;

for(k=1;k<=e;k++)
{ printf(“\n vi,vj= ”);
scanf(“%d%d”, &i,&j);
G[i][j]=1; G[j][i]=1;

}
}

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();
}
图的邻接链表存储及递归深度优先遍历。
# include
# include
# define MAX 20
typedef int VexType;
typedef struct Vnode
{ VexType data;
struct Vnode *next;
}Vnode;
typedef Vnode Lgraph[MAX];

void creat_L(Lgraph G);
void out_L(Lgraph G);
void dfsL(Lgraph G,int v);
Lgraph Ga;
int n,e, vis[MAX];

main()
{ int v1,i; char ch;
for(i=0;icreat_L(Ga);
out_L(Ga);
printf("\n "); scanf("%d",&v1);
dfsL(Ga,v1);
printf("\n\n 打回车键,继续。“); ch=getch();
}

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;
q=(Vnode *)malloc(sizeof(Vnode));
q->data=j;
q->next=G[i].next; G[i].next=q;
}
}

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();
}

void dfsL(Lgraph G,int v)
{ Vnode *p;
printf("%3d",G[v].data); vis[v]=1;
p=G[v].next;
while(p){ v=p->data;
if(vis[v]==0)dfs(G,v);
p=p->next;
}
}
注:图的广度优先遍用非递归方法容易理解,非递归方法需要辅助队列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();
}
三、实习题
阅读理解上面第一个关于图的邻接矩阵的程序,做下列题目。
根据教科书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();
}
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);
if(f=NULL)t=s;
else { if(k<=f->data) f->lch=s;
else f->rch=s;
}
printf(“\n Key= ”);scanf(“%d”,&k);
}
return(t);
}

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);
}

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;
else if(k<=q->data)q=q->lch;
else q=q->rch;
}
if(tag==1)printf(“\n yes!”);
else printf(“\n no!”);
}

void display(BiTNode *t)
{ if( t!=NULL) { display(tlc);
printf(“name:%s\t tel:%s\n”,tname,ttel);
display(trc);
}
}
三、实习题
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),若发生冲突后, 用线性探测再散列方法解决冲突。
实习九 排序
一、实习目的
熟悉几种典型的排序方法,并对各种算法的特点、使用范围和效率有进一步的
了解。
二、实例
排序是计算机科学中非常基本且使用频繁的运算,在计算机系统软件和应用软件中都有广泛的应用。下面给出的是冒泡排序、快速排序的实现与比较。

void getrandat(Data ary[],int count)
{ long int a=100001;
int i;
for(i=0;iary[i].key=(int)a;
}
}
void prdata(Data ary[],int count)
{ int i; char ch;
printf(“\n”);
for(i=0;iprintf(“\n”);
printf(“\n\n 打回车键,结束显示。“); ch=getch();
}
[两种排序方法的比较源程序]

#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);
for(j=0;jprdata(ar,n);
} break;
case 2:{ for(j=0;jbubble(ar,n);
prdata(ar,n);
} break;
case 3: { bubble( ar,n);
prdata(ar,n);
} break;
case 4:{ for(j=0;jqksort(ar,n);
prdata(ar,n);
} break;
case 5:{ qksort(ar,n);
prdata(ar,n);
} break;
case 6: exit(0);
}
printf("\n ----------------");
}while(k>=1 && k<6);
printf("\n 再见!")
printf(“\n 打回车键,返回。“); ch=getch();
}

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}

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);
}

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);
}

void init_s(SqStack *s)
{ s->top=0;
} ;

void push(SqStack *s,Selem e)
{ if(s->top==MAX-1)printf(“\n stack Overflow!\n”);
else { s->top++;
s->elem[s->top]=e;
}
}

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);
}
本程序中,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++;
s+=i;
}
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. D
9. B 10. B
1.2 1. 线性结构、树形结构、图形结构、非线性结构
2. 没有、1、没有、1
3. 前驱、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. 120
2. 一个栈的入栈序列a,b,c,d,e,则栈的不可能的输出序列是____。
A. edcba B. decba C. dceab D. abcde
3. 若已知一个栈的入栈序列是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= =0
C. ST—> top !=m0 D. ST—> top= =m0
6. 判定一个栈ST(最多元素为m0)为栈满的条件是____。
A. ST—> top!=0 B. ST—> top= =0
C. ST—> top!=m0 D. ST—> top= =m0
7. 栈的特点是____,队列的特点是____。
A. 先进先出 B. 先进后出
8. 一个队列的入列序列是1,2,3,4,则队列的输出序列是____ 。
A. 4,3,2,1 B. 1,2,3,4
C. 1,4,3,2 D. 3,2,4,1
9. 判定一个队列QU(最多元素为m0)为空的条件是____。
QU—>rear—QU—>front= =m0
QU—>rear—QU—>front-1= =m0
QU—>front= =QU—>rear
QU—>front= =QU—>rear+1
10. 判定一个队列QU(最多元素为m0, m0+1= =Maxsize)为满队列的条件是____。
((QU—>rear-QU—>front)+ Maxsize)% Maxsize = =m0
QU—>rear—QU—>front-1= =m0
QU—>front= =QU—>rear
QU—>front= =QU—>rear+1
11. 判定一个循环队列QU(最多元素为m0)为空的条件是____。
QU—>front= =QU—>rear
QU—>front!=QU—>rear
QU—>front= =(QU—>rear+1)%m0
QU—>front!=(QU—>rear+1)%m0
12. 判定一个循环队列QU(最多元素为m0)为满队列的条件是____。
QU—>front= =QU—>rear
QU—>front!=QU—>rear
QU—>front= =(QU—>rear+1)%m0
QU—>front!=(QU—>rear+1)%m0
13. 循环队列用数组A[0,m-1]存放其元素值,已知其头尾指针分别是front和rear,则当前队列中的元素个数是____。
A. (rear-front+m)%m B. rear-front+1
C. rear-front-1 D. rear-front
14. 栈和队列的共同点是____。
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 List
InitList( &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)→e
LocateElem(LA, e, equal( ))
ListInsert(LA, n+1, e)
操作步骤:
GetElem(Lb, i, e); // 取Lb中第i个数据元素赋给e
if (!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);
} // union
GetElem(Lb, i, e); // 取Lb中第 i 个数据元素赋给 e
if (!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++) {
}
} // purge
GetElem(Lb, i, e); // 取Lb中第i个数据元素赋给 e
if (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-3
k = 1, 2, …, m+n
1.初始化 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 = 0
GetElem(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_list
while ((i <= La_len) && (j <= Lb_len))
{ // La 和 Lb 均不空 }
while (i<=La_len) // 若 La 不空
while (j<=Lb_len) // 若 Lb 不空
InitList(Lc); // 构造空的线性表 Lc
i = 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_Sq
O( 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 17
L.elem
L.length = 7
L.listsize
e =
38
p
p
p
p
p
i
1
2
3
4
1
8
50
p
可见,基本操作是,
将顺序表中的元素
逐个和给定值 e
相比较。
线性表操作
ListInsert(&L, i, e)的实现:
首先分析:
插入元素时,
线性表的逻辑结构发生什么变化?
(a1, …, ai-1, ai, …, an) 改变为
a1 a2 … ai-1 ai … an
a1 a2 … ai-1

ai
e
an

,
表的长度增加
(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; // 表长增1
return 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 87
21 18 30 75
例如:ListInsert_Sq(L, 5, 66)
L.length-1
0
p
p
p
q
87
56
42
66
q = &(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 … an
a1 a2 … ai-1
(a1, …, ai-1, ai+1, …, an)
Status ListDelete_Sq
(SqList &L, int i, ElemType &e) {
} // ListDelete_Sq
for (++p; p <= q; ++p) *(p-1) = *p;
// 被删除元素之后的元素左移
--L.length; // 表长减1
return OK;
算法时间复杂度为:
O( ListLength(L))
p = &(L.elem[i-1]); // p 为被删除元素的位置
e = *p; // 被删除元素的值赋给 e
q = L.elem+L.length-1; // 表尾元素的位置
if ((i < 1) || (i > L.length)) return ERROR;
// 删除位置不合法
元素左移
考虑移动元素的平均情况:
假设删除第 i 个元素的概率为 ,
则在长度为n 的线性表中删除一个元素所需移动元素次数的期望值为:
若假定在线性表中任何一个位置上进行删除的概率都是相等的,则移动元素的期望值为:
21 18 30 75 42 56 87
21 18 30 75
L.length-1
0
p
p
p
q
87
56
p = &(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)
在单链表中的实现:
21
18
30
75
42
56

p
p
p
j
1
2
3
因此,查找第 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)
在单链表中的实现:
有序对
改变为
e
ai
ai-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 大于表长或者小于1
s = new LNode;
// 生成新结点
s->data = e;
s->next = p->next; p->next = s; // 插入
return OK;
e
ai-1
ai
ai-1
s
p
线性表的操作ListDelete (&L, i, &e)在链表中的实现:
有序对
改变为
ai-1
ai
ai+1
ai-1
在单链表中删除第 i 个结点的基本操作为:找到线性表中第i-1个结点,修改其指向后继的指针。
ai-1
ai
ai+1
ai-1
q = p->next; p->next = q->next;
e = q->data; free(q);
p
q
Status 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;
}
} // ClearList
free(p);
算法时间复杂度:
O(ListLength(L))
如何从线性表得到单链表?
链表是一个动态的结构,它不需要予分配空间,因此生成链表的过程是一个结点“逐个插入” 的过程。
例如:逆位序输入 n 个数据元素的值,
建立带头结点的单链表。
操作步骤:
一、建立一个“空表”;
二、输入数据元素an,
建立结点并插入;
三、输入数据元素an-1,
建立结点并插入;
an
an
an-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;
// 若分配失败,则返回 ERROR
void 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; 否则返回ERROR
Status 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。
} // InsAfter
if ( ! 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。
} //DelAfter
if ( !(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 表中第一个元素 a
else a = MAXC; // MAXC为常量最大值
if ( DelFirst( Lb, e)) b = e; // 取得 Lb 表中第一个元素 b
else b = MAXC; // a 和 b 为两表中当前比较元素
DestroyList(La); DestroyList(Lb); // 销毁链表 La 和 Lb
return OK;
if ((*compare)(a, b) <=0) { // a≤b
InsAfter(Lc, a);
if ( DelAfter( La, e1) ) a = e1;
else a = MAXC;
}
else { // a>b
InsAfter(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 … ... an
2. 循环链表
和单链表的差别仅在于,判别链表中最后一个结点的条件不再是“后继是否为空”,而是“后继是否为头结点”。
双向循环链表
空表
非空表
a1 a2 … ... an
双向链表的操作特点:
“查询” 和单链表相同
“插入” 和“删除”时需要同时修改两个方向上的指针。
ai-1
ai
e
s->next = p->next; p->next = s;
s->next->prior = s; s->prior = p;
p
s
ai-1
ai
插入
ai-1
删除
ai
ai+1
p->next = p->next->next;
p->next->prior = p;
p
ai-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); // 构造(空的)线性表LA
La_len=ListLength(La); Lb_len=ListLength(Lb);
for (i = 1; i <= Lb_len; i++) {
GetElem(Lb, i, e); // 取Lb中第 i 个数据元素赋给 e
if (!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++) {
}
} // purge
GetElem(Lb, i, e); // 取Lb中第i个数据元素赋给 e
if (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≥0
TermSet 中的每个元素包含一个
表示系数的实数和表示指数的整数 }
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
} // CreatPolyn
InitList (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)) {
… …
}
… …
} // AddPolyn
switch (*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");
else
printf("结点未找到!\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++)
{ p=head;
head=head->next;
free(p);
}
else
{ p=head;
for(j=1;j<=i-2;j++)
p=p->next;
for(j=1;j<=length;j++)
{
q=p->next;
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)

{
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;
odd->next= even ->next;
temp->next= even;
temp= even;
odd=odd->next;
}
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.文件的组织方式:
主文件按柱面集中存放,同时建立
三级索引:磁道索引、柱面索引和
主索引。
关键字 指针 关键字 指针
磁道索引结构
基本索引项
溢出索引项
210
1024



r(14) r(21) r(38)
r(41) r(57) r(63)
r(72) r(85) r(99)
溢 出 区
磁 道 索 引
r(514) … …
溢 出 区
磁道索引
… … r(1024)
一 个 柱 面
….




99
210
1024
T0
T1
T2
T3
T4
T5
2.操作的特点:
检 索
插入
删除
检索:
可有两种方式:
按关键字存取— 从主索引开始,到
柱面索引,到磁道索引,最后取
得记录,先后访问四次外存。
顺序存取— 依关键字最小至大顺序
存取
插入:
修改本磁道的索引项(包括基本索
引项和溢出索引项)。
将该磁道上关键字最大的记录移出
到本柱面的溢出区中;
将记录插入在某个磁道的合适位置上;
删除:
在被删记录当前存储位置上
作“删除标记”。
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 182
589 505 764
269
166
330
溢出桶
在哈希文件中,“冲突”和“溢出”
是不同的概念。一般情况下,假设桶
的大小为m,则允许哈希地址产生m-1
次的冲突,当发生第m次冲突时,才
需要进行“冲突处理”,对散列文件而
言,通常采用链地址法出路冲突。为
区别起见,称直接“散列”的数据块为
“基桶”,而因“溢出”存放的数据块为
“溢出桶”。
3.文件的操作
检索:只能进行按关键字的查找,不能进行顺序查找。检索时,先在基桶内进行查找,若不存在,则再到溢出桶中进行查找。
插入:当查找不成功时,将记录插入在相应的基桶或溢出桶内。
删除:对被删记录作特殊标记。
4.
优点:记录随机存放,不需要进行排
序;插入、删除方便,存取速
度快;节省存储空间,不需要
索引区。
缺点:不能进行顺序存取;在经过多
次插入和删除操作之后,需进
行“重组文件”的操作。
12.6 多 关 键 字 文 件
一、多关键字文件的特点
除需要对主关键字建立“主索引”
外, 尚需对各个次关键字建立“次索引”。
次索引项: 次关键字 (指向记录的)指针
二、次索引的组织方法
1.多重链表文件 特点:将所有具有相同次关键字的记录链接在同一链表中,该链表的头指针即为次索引项中“指针域”的值。
2.倒排文件 特点:将所有具有相同次关键字的记录构成一个次索引顺序表,此时的次索引顺序表中仅存放记录的“主关键字”或记录的“物理记录号”。次索引项中的“指针”指向相应的次索引顺序表。
3.次关键字索引表本身的结构 可以是顺序表,也可以是树表或哈希表,视具体的次关键字的特性而定。
本章学习要求:
熟悉各类文件的特点,构造方法以及如何实现检索,插入和删除等操作。(共87张PPT)
数据结构
主讲 陈自刚
E-mail: zgchen@
Tel: 13323689160
1.1 数据结构讨论的范畴
1.2 基本概念
1.3 算法和算法的量度
知 识 点
数据结构中常用的基本概念和术语
算法描述和分析方法
难 点
算法复杂性的分析方法
要 求
了解数据的逻辑结构和物理结构,算法的基本概念,它们对于程序设计的重要性以及相互关系
掌握算法复杂性的概念及分析方法
1.1 数据结构讨论的范畴
Niklaus Wirth
Algorithm + 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,9345
a1 a2 a3
6587,3214,9345
a2 a1 a3

例如:
又例,在2行3列的二维数组
{a1, a2, a3, a4, a5, a6}中六个
元素之间存在两个关系:
行的次序关系:
列的次序关系:
row = {,,,}
col = {,,}
a1 a3 a5
a2 a4 a6
a1 a2 a3
a4 a5 a6
a1 a2 a3
a4 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)2
A = (101)8 = (001000001)2
关系的映象方法:
(表示 x, y 的方法)
顺序映象
以相对的存储位置表示后继关系
例如:令 y 的存储位置和 x 的存储位置之间差一个常量 C
是一个隐含值,整个存储结构中只含数据元素本身的信息而 C
x y
链式映象
以附加信息(指针)表示后继关系
需要用一个和 x 在一起的附加信息指示 y 的存储位置
y x
在不同的编程环境中,
存储结构可有不同的描述方法,
当用高级程序设计语言进行编程时,通常可用高级编程语言中提供的数据类型描述之。
例如:
以三个带有次序关系的整数表示一个长整数时,可利用 C 语言中提供的整数数组类型,
typedef int Long_int [3]
定义长为整数:
typedef struct {
int y; // 年号 Year
int m; // 月号 Month
int 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*10
3.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 5
i n t(整型) 1 6 - 3 2 7 6 8 ~ 3 2 7 6 7
f 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 5
signed char(有符号字符型 ) 8 - 1 2 8 ~ 1 2 7
i n t(整型) 1 6 3 2 7 6 8 ~ 3 2 7 6 7
unsigned int(无符号整型 ) 1 6 0 ~ 6 5 5 3 5
signed int(有符号整型 ) 1 6 同i n t
第2章 数据类型、运算符和表达式 1 5
下载
(续)
类 型 长 度(b i t) 范 围
short int(短整型) 8 1 2 8 ~ 1 2 7
unsigned short int(无符号短整型 ) 8 0 ~ 2 5 5
signed short int(有符号短整型 ) 8 同s h o r t i n t
long int(长整型) 3 2 2 1 4 7 4 8 3 6 4 8 ~ 2 1 4 7 4 8 3 6 4 9
signed long int(有符号长整型 ) 3 2 2 1 4 7 4 8 3 6 4 8 ~ 2 1 4 7 4 8 3 6 4 9
unsigned long int(无符号长整型 ) 3 2 0 ~ 4 2 9 4 9 6 7 2 9 6
f 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 t
t e s t 2 3 hi! there
h i g h _ b a l a n c e h i g h . . b a l a n c e
A 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 2
A 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 4
long int 3 5 0 0 0、 -3 4
short int 1 0、-1 2、9 0
unsigned int 1 0 0 0 0、 9 8 7、 4 0 0 0 0
f l o a t 1 2 3 . 2 3、 4 . 3 4 e-3
d 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 4
C语言还支持另一种预定义数据类型的常量,这就是串。所有串常量括在双撇号之间,例
如"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 s
0X 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或0XA
1 3 2 0 2 0 4 0X8 4
3 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;
unsigned 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=-14
1 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 3
0 4 8
1 4 9
9 5 7
A 6 5
B 6 6
a 9 7
b 9 8
注意 字符' 9 '和数字9的区别,前者是字符常量,后者是整型常量,它们的含义和在计
算机中的存储方式都截然不同。
由于C语言中字符常量是按整数( s h o r t型)存储的,所以字符常量可以像整数一样在程序
中参与相关的运算。例如:
' a '-3 2 ;
pr i n t f ( " % d " , x % y ) ;
x= 1 ;
y= 2 ;
pr i n t f ( " % d , % d " , x / y , x % y ) ;
最后一行打印一个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 < = 4
2 4 C语言程序设计
下载
p q p & & q p | | q ! p
0 0 0 0 1
0 1 0 1 1
1 1 1 1 0
1 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 q
0 0 0
1 0 1
1 1 0
0 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,其中奇偶校验位为 1
0 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 7
x < < 1 0 0 0 0 111 0 1 4
x < < 3 0 111 0 0 0 0 11 2
x < < 2 11 0 0 0 0 0 0 1 9 2
x > > 1 0 11 0 0 0 0 0 9 6
x > > 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 3
E 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 double
int double double
d o u b l e
d 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 +1
R[0]
j
R[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]
j
R[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) {
}
} // InsertSort
L.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 ) {
} // for
L.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; // 插入点在高半区
i
low
high
m
m
low
low
m
high
i
low
high
m
high
m
high
m
low
例如:
再如:
插入
位置
插入
位置
14 36 49 52 80
58 61 23 97 75
L.r
14 36 49 52 58 61 80
23 97 75
L.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; // 指向被移走的记录, }//if
p = 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 =5
11 23 12 9 18 16 25 36 30 47 31
第二趟希尔排序,设增量 d = 3
9 18 12 11 23 16 25 31 30 47 36
第三趟希尔排序,设增量 d = 1
9 11 12 16 18 23 25 30 31 36 47
void 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
} // ShellInsert
void ShellSort (SqList &L, int dlta[], int t)
{ // 增量为dlta[]的希尔排序
for (k=0; kShellInsert(L, dlta[k]);
//一趟增量为dlta[k]的插入排序
} // ShellSort
10.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
} // BubbleSort
i = n;
i = lastExchangeIndex; // 本趟进行过交换的
// 最后一个记录的位置
if (R[j+1].key < R[j].key) {
Swap(R[j], R[j+1]);
lastExchangeIndex = j; //记下进行交换的记录位置
} //if
for (j = 1; j < i; j++)
lastExchangeIndex = 1;
注意:
2. 一般情况下,每经过一趟“起泡”,“i 减一”,但并不是每趟都如此。
例如:
2
5
5
3
1
5
7
9
8
9
i=7
i=6
for (j = 1; j < i; j++) if (R[j+1].key < R[j].key) …
1
3
i=2
1. 起泡排序的结束条件为,
最后一趟没有进行“交换记录”。
时间分析:
最好的情况(关键字在记录序列中顺序有序):
只需进行一趟起泡
“比较”的次数:
最坏的情况(关键字在记录序列中逆序有序):
需进行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)
s
t
low
high
设 R[s]=52 为枢轴暂存在R[0]的位置上
将 R[high].key 和 枢轴的关键字进行比较,要求R[high].key ≥ 枢轴的关键字
将 R[low].key 和 枢轴的关键字进行比较,要求R[low].key ≤ 枢轴的关键字
high
23
low
80
high
14
low
52
例如
R[0]
52
low
high
high
high
low
可见,经过“一次划分” ,将关键字序列
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; // 返回枢轴所在位置
} // Partition
int Partition (RedType R[], int low, int high) {
}// Partition
R[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
}
} // QSort
pivotloc = 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 小的记录,并交换到位
}
} // SelectSort
j = 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}
不是堆
(小顶堆)
(大顶堆)
ri
r2i
r2i+1
若将该数列视作完全二叉树,
则 r2i 是 ri 的左孩子; r2i+1 是 ri 的右孩子。
12
36
27
65
49
81
73
55
40
34
98
例如:
是堆
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 进行堆排序。
} // HeapSort
for ( 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;
// 堆采用顺序表表示之
所谓“筛选”指的是,对一棵左/右子树
均为堆的完全二叉树,“调整”根结点
使整个二叉树也成为一个堆。


筛选
98
81
49
73
55
64
12
36
27
40
例如:
是大顶堆
12
但在 98 和 12 进行互换之后,它就不是堆了
因此,需要对它进行“筛选”
98
12
81
73
64
12
98
比较
比较
void HeapAdjust (RcdType &R[], int s, int m)
{ // 已知 R[s..m]中记录的关键字除 R[s] 之外均
// 满足堆的特征,本函数自上而下调整 R[s] 的
// 关键字,使 R[s..m] 也成为一个大顶堆。
} // HeapAdjust
rc = 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 指示关键字较大记录的位置
建堆是一个从下往上进行“筛选”的过程。
40
55
49
73
81
64
36
12
27
98
例如: 排序之前的关键字序列为
12
36
81
73
49
98
81
73
55
现在,左/右子树都已经调整为堆,最后只要调整根结点,使整个二叉树是个“堆”即可。
98
49
40
64
36
12
27
堆排序的时间复杂度分析:
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]
} // Merge
for (j=m+1, k=i; i<=m && j<=n; ++k)
{ // 将SR中记录由小到大地并入TR
if (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] 复制到 TR
if (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,30
1,2,15
3,1,20
2,3,18
2,1,20
1,2,15
2,3,18
3,1,20
2,1,20
3,2,30
3,1,20
2,1,20
1,2,15
3,2,30
2,3,18
1,2,15
2,1,20
2,3,18
3,1,20
3,2,30
LSD的排序过程如下:
二、链式基数排序
  假如多关键字的记录序列中,每个关键字的取值范围相同,则按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→139
p→230→367→167→237→138→368→239→139
f[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 ←
→368
p→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< K3
K2K1K3K2K3K11.树上的每一次“比较”都是必要的;
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,996
5. 答:采用插入排序法排序时各趟的结果如下:
初始:(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;
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;
请问队列为空和队列为满分别满足什么条件?
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)
8
16
32
64
0
范 围
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)
8
8
8
16
16
16
范 围
ASCII字符
0~255
-128~127
32768~32767
0~65535
同int
下载
第2章 数据类型、运算符和表达式15
(续)
类 型
short int(短整型)
unsigned short int(无符号短整型)
signed short int(有符号短整型 )
long int(长整型)
signed long int(有符号长整型 )
unsigned long int(无符号长整型)
float(单精度型)
double(双精度型)
长 度(bit)
8
8
8
32
32
32
32
64
范 围
128~127
0~255
同shortint
2147483648~2147483649
2147483648~2147483649
0~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语言编译器可能不允许下划线作为标识符的起始字
符)。下面是一些正确或错误标识符命名的实例。
正确形式
count
test23
high_balance
错误形式
2count
hi! there
high..balance
ANSI标准规定,标识符可以为任意长度,但外部名必须至少能由前 8个字符唯一地区分。
这里外部名指的是在链接过程中所涉及的标识符,其中包括文件间共享的函数名和全局变量
名。这是因为对某些仅能识别前 8个字符的编译程序而言,下面的外部名将被当作同一个标识
符处理。
counters
counters1
counters2
ANSI标准还规定内部名必须至少能由前 31个字符唯一地区分。内部名指的是仅出现于定
16C语言程序设计
义该标识符的文件中的那些标识符。
下载
C语言中的字母是有大小写区别的,因此 count Count COUNT是三个不同的标识符。
标识符不能和C语言的关键字相同,也不能和用户已编制的函数或 C语言库函数同名。
2.2.2 常量
C语言中的常量是不接受程序修改的固定值,常量可为任意数据类型,如下例所示 :
数据类型
char
int
long int
short int
unsigned int
float
double
常量举例
'a'、'\n'、'9'
21、 123 、2100 、-234
35000、 -34
10、-12、90
10000、 987、 40000
123.23、 4.34e-3
123.23、 12312333、 -0.9876234
C语言还支持另一种预定义数据类型的常量,这就是串。所有串常量括在双撇号之间,例
如"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是必须的引导符。
十六进制常数是下述形式:
0xhdigits
0Xhdigits
这里hdigits可以是一个或多个十六进制数(从 0~9的数字,并从“ a”~“ f”的字母)。
引导符0是必须有的,X即字母可用大写或小写。
注意,空白字符不可出现在整数数字之间。表 2-3列出了整常数的形式。
表2-3 整常数的例子
十 进 制
10
132
32179
八 进 制
012
0204
076663
十 六 进 制
0Xa或0XA
0X84
0X7db3或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);


运行结果为 :
RUN
a+u=22, b+u=-14
18C语言程序设计
下载
可以看到不同类型的整型数据可以进行算术运算。在本例中是 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;


在一般系统中,一个 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码值(十进制)
!
0
1
9
A
B
a
b
33
48
49
57
65
66
97
98
注意 字符'9'和数字9的区别,前者是字符常量,后者是整型常量,它们的含义和在计
算机中的存储方式都截然不同。
由于C语言中字符常量是按整数( short型)存储的,所以字符常量可以像整数一样在程序
中参与相关的运算。例如:
'a'-32;
'A' + 32;
'9'-9;
2.5.2 字符串常量

*/
*/
字符串常量是指用一对双引号括起来的一串字符。双引号只起定界作用,双引号括起的
字符串中不能是双引号( ")和反斜杠(\),它们特有的表示法在转义字符中介绍。例如:
"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码值(十进制)
007
008
012
010
013
009
011
092
063
039
034
000
三位八进制
二位十六进制
字符常量中使用单引号和反斜杠以及字符常量中使用双引号和反斜杠时,都必须使用转
义字符表示,即在这些字符前加上反斜杠。
在C程序中使用转义字符 \ddd 或者\xhh可以方便灵活地表示任意字符。 \ddd为斜杠后面跟
三位八进制数,该三位八进制数的值即为对应的八进制 ASCII码值。\x后面跟两位十六进制数,
该两位十六进制数为对应字符的十六进制 ASCII码值。
使用转义字符时需要注意以下问题:
1) 转义字符中只能使用小写字母,每个转义字符只能看作一个字符。
2) \v 垂直制表和 \f 换页符对屏幕没有任何影响,但会影响打印机执行响应操作。
3) 在C程序中,使用不可打印字符时,通常用转义字符表示。
2.5.4 符号常量
C语言允许将程序中的常量定义为一个标识符,称为符号常量。符号常量一般使用大写英
文字母表示,以区别于一般用小写字母表示的变量。符号常量在使用前必须先定义,定义的
形式是:
#define <符号常量名> <常量>
例如:
#define PI
3.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
A
B
它的作用是将两个小写字母转换为大写字母。因为 '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);



最后一行打印一个 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<=4
24C语言程序设计
下载
p
0
0
1
1
这一表达式的结果为 True。
q
0
1
1
0
p&&q
0
0
1
0
p||q
0
1
1
1
!p
1
1
0
0
下表给出了关系和逻辑运算符的相对优先级:
最高 !
>= <=
== !=
&&
最低 ||
同算术表达式一样,在关系或逻辑表达式中也使用括号来修改原计算顺序。
切记,所有关系和逻辑表达式产生的结果不是 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)
右移
左移
P
0
1
1
0
表2-8 异或的真值表
q
0
0
1
1
p^q
0
1
0
1
下载
第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,其中奇偶校验位为 1
011111111 二进制的 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 用移位操作进行乘和除
字 符 x
x=7
x<<1
x<<3
x<<2
x>>1
x>>2
每个语句执行后的x
00000111
00001110
01110000
11000000
01100000
00011000
x 的 值
7
14
112
192
96
24
每左移一位乘 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章 数据类型、运算符和表达式27
EXP1,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 double
int double double
double
double
图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 图的定义及术语:
由于“弧”是有方向的,因此称由顶点集和弧集构成的图为有向图。
E
A
C
B
D
例如:
G1 = (V1, VR1)
其中
V1={A, B, C, D, E}
VR1={, ,
, , ,
, }
VR 必有 VR,
则称 (v,w) 为顶点 v 和顶点 w 之间存在一条边。
B
C
A
F
E
D
由顶点集和边集构成的图称作无向图。
例如: 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) }
名词和术语
网、子图
完全图、稀疏图、稠密图
邻接点、度、入度、出度
路径、路径长度、简单路径、简单回路
连通图、连通分量、
强连通图、强连通分量
生成树、生成森林
A
B
E
C
F
A
E
A
B
B
C
设图G=(V,{VR}) 和图 G =(V ,{VR }),
且 V V, VR VR,
则称 G 为 G 的子图。
15
9
7
21
11
3
2
弧或边带权的图分别称作有向网或无向网。
假设图中有 n 个顶点,e 条边,则
含有 e=n(n-1)/2 条边的无向图称作完全图;
含有 e=n(n-1) 条弧的有向图称作 有向完全图;
若边或弧的个数 e假若顶点v 和顶点w 之间存在一条边,
则称顶点v 和w 互为邻接点,
例如:
ID(B) = 3
ID(A) = 2
边(v,w) 和顶点v 和w 相关联。
和顶点v 关联的边的数目定义为边的度。
A
C
D
F
E
B
右侧图中
顶点的出度: 以顶点v 为弧尾的弧的数目;
A
B
E
C
F
对有向图来说,
顶点的入度: 以顶点v为弧头的弧的数目。
顶点的度(TD)=
出度(OD)+入度(ID)
例如:
ID(B) = 2
OD(B) = 1
TD(B) = 3
由于弧有方向性,则有入度和出度之分
设图G=(V,{VR})中的一个顶点序列
{ u=vi,0,vi,1, …, vi,m=w}中,(vi,j-1,vi,j) VR 1≤j≤m,
则称从顶点u 到顶点w 之间存在一条路径。
路径上边的数目称作路径长度。
A
B
E
C
F
如:从A到F长度为 3 的路径{A,B,C,F}
简单路径:指序列中顶点不重复出现的路径。
简单回路:指序列中第一个顶点和最后一个顶点相同的路径。
若图G中任意两个顶点之间都有路径相通,则称此图为连通图;
若无向图为非连通图,则图中各个极大连通子图称作此图的连通分量。
B
A
C
D
F
E
B
A
C
D
F
E
若任意两个顶点之间都存在一条有向路径,则称此有向图为强连通图。
A
B
E
C
F
A
B
E
C
F
对有向图,
否则,其各个强连通子图称作它的强连通分量。
假设一个连通图有 n 个顶点和 e 条边,其中 n-1 条边和 n 个顶点构成一个极小连通子图,称该极小连通子图为此连通图的生成树。
对非连通图,则称由各个连通分量的生成树的集合为此非连通图的生成森林。
B
A
C
D
F
E
结构的建立和销毁
插入或删除顶点
对邻接点的操作
对顶点的访问操作
遍历
插入和删除弧
基本操作
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) VR
1 (i,j) VR
一、图的数组(邻接矩阵)存储表示
B
A
C
D
F
E
定义:矩阵的元素为
有向图的邻接矩阵为非对称矩阵
A
B
E
C
F
typedef 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;
D
B
A
C
F
E
二、图的邻接表
存储表示
A 1 4
B 0 4 5
C 3 5
D 2 5
E 0 1
F 1 2 3
0 1 2 3 4 5
有向图的邻接表
1 4
2
3
0 1
2
0 1 2 3 4
A
B
C
D
E





A
B
E
C
F
可见,在有向图的邻接表中不易找到指向该顶点的弧
A
B
E
C
D
有向图的逆邻接表
A
B
C
D
E
3
0
3
4
2
0





0
1
2
3
4
在有向图的邻接表中,对每个顶点,链接的是指向该顶点的弧
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;
图的结构定义(邻接表)
三、有向图的十字链表存储表示
A
B
C
A
B
C
0 1 2

0 2 ∧ ∧
0 1
2 1 ∧
2 0 ∧ ∧
弧的结点结构
弧尾顶点位置 弧头顶点位置 弧的相关信息
指向下一个有相同弧尾的结点
指向下一个有相同弧头的结点
typedef struct ArcBox { // 弧的结构表示
int tailvex, headvex; InfoType *info;
struct ArcBox *hlink, *tlink;
} VexNode;
tailvex
headvex
hlink
tlink
info
顶点的结点结构
顶点信息数据
指向该顶点的第一条入弧
指向该顶点的第一条出弧
typedef struct VexNode { // 顶点的结构表示
VertexType data;
ArcBox *firstin, *firstout;
} VexNode;
data
firstin
firstout
typedef 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有路径相通的顶点都被访问到。
一、深度优先搜索遍历图
连通图的深度优先搜索遍历
SG1
SG2
SG3
W1、W2和W3 均为 V 的邻接点,SG1、SG2 和 SG3 分别为含顶点W1、W2和W3 的子图。
访问顶点 V ;
for (W1、W2、W3 )
若该邻接点W未被访问,
则从它出发进行深度优先搜索遍历。
V
w1
w3
w2
从上页的图解可见:
1. 从深度优先搜索遍历连通图的过程类似于树的先根遍历;
解决的办法是:为每个顶点设立一个 “访问标志 visited[w]”;
2. 如何判别V的邻接点是否被访问?
void DFS(Graph G, int v) {
// 从顶点v出发,深度优先搜索遍历连通图 G
visited[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
}
a
b
c
h
d
e
k
f
g
F F F F F F F F F
T
T
T
T
T
T
T
T
T
a
c
h
d
k
f
e
b
g
a
c
h
k
f
e
d
b
g
访问标志:
访问次序:
例如:
0 1 2 3 4 5 6 7 8
8
1
2
3
4
5
6
7
0
二、广度优先搜索遍历图
V
w1
w8
w3
w7
w6
w2
w5
w4
对连通图,从起始点V到其余各顶点必定存在路径。
其中,V->w1, V->w2, V->w8
的路径长度为1;
V->w7, V->w3, V->w5
的路径长度为2;
V->w6, V->w4
的路径长度为3。
w1
V
w2
w7
w6
w3
w8
w5
w4
从图中的某个顶点V0出发,并在访问此顶点之后依次访问V0的所有未被访问过的邻接点,之后按这些顶点被访问的先后次序依次访问它们的邻接点,直至图中所有和V0有路径相通的顶点都被访问到。
若此时图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。
void BFSTraverse(Graph G,
Status (*Visit)(int v)){
for (v=0; vvisited[v] = FALSE; //初始化访问标志
InitQueue(Q); // 置空的辅助队列Q
for ( v=0; vif ( !visited[v]) { // v 尚未访问
}
} // BFSTraverse
… …
visited[v] = TRUE; Visit(v); // 访问u
EnQueue(Q, v); // v入队列
while (!QueueEmpty(Q)) {
DeQueue(Q, u);
// 队头元素出队并置为u
for(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 的简单路径
a
b
c
h
d
e
k
f
g
求从顶点 b 到顶点 k 的一条简单路径。
从顶点 b 出发进行深度优先搜索遍历
例如:
假设找到的第一个邻接点是a,且得到的结点访问序列为: b a d h c e k f g
假设找到的第一个邻接点是c,则得到的结点访问序列为:
b c h d a e k f g
1. 从顶点 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个顶点加入路径
&&!found
if (w=s) { found = TRUE; Append(PATH, w); }
else
if (!found) Delete (PATH); // 从路径上删除顶点 v
2. 求两个顶点之间的一条路径
长度最短的路径
若两个顶点之间存在多条路径,则其中必有一条路径长度最短的路径。如何求得这条路径
因此,求路径长度最短的路径可以基于广度优先搜索遍历进行,但需要修改链队列的结点结构及其入队列和出队列的算法。
a
b
c
h
d
e
k
f
g
深度优先搜索访问顶点的次序取决于图的存储结构,而广度优先搜索访问顶点的次序是按“路径长度”渐增的次序。
例如:求右图中顶点 3 至顶点 5 的一条最短路径。
链队列的状态如下所示:
Q.front
3 1 2 4 7 5
3
2
1
4
7
5
6
8
9
Q.rear
1) 将链队列的结点改为“双链”结点。即
结点中包含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 个顶点为止。
普里姆算法的基本思想:
a
b
c
d
e
g
f
19
5
14
18
27
16
8
21
3
12
7
例如:
a
e
d
c
b
g
f
14
8
5
3
16
21
所得生成树权值和
= 14+8+3+5+16+21 = 67
在生成树的构造过程中,图中 n 个顶点分属两个集合:已落在生成树上的顶点集 U 和尚未落在生成树上的顶点集V-U ,则应在所有连通U中顶点和V-U中顶点的边中选取权值最小的边。
一般情况下所添加的顶点应满足下列条件:
U
V-U
设置一个辅助数组,对当前V-U集中的每个顶点,记录和顶点集U中顶点相连接的代价最小的边:
struct {
VertexType adjvex; // U集中的顶点序号
VRType lowcost; // 边的权值
} closedge[MAX_VERTEX_NUM];
a
b
c
d
e
g
f
19
5
14
18
27
16
8
21
3
12
7
a
e
d
c
b
a
a
a
19
14
18
14
例如:
e
12
e
e
8
16
8
d
3
d
d
7
21
3
c
5
5
19 m m 14 m 18
19 5 7 12 m m
m 5 3 m m m
m 7 3 8 21 m
14 12 m 8 m 16
m m m 21 m 27
18 m m m 16 27
void 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 条边为止。
考虑问题的出发点: 为使生成树上边的权值之和达到最小,则应使生成树中每一条边的权值尽可能地小。
克鲁斯卡尔算法的基本思想:
a
b
c
d
e
g
f
19
5
14
18
27
16
8
21
3
a
e
12
d
c
b
g
f
7
14
8
5
3
16
21
例如:
7
12
18
19
算法描述:
构造非连通图 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 拓扑排序
问题:
假设以有向图表示一个工程的施工图或程序的数据流图,则图中不允许出现回路。
检查有向图中是否存在回路的方法之一,是对有向图进行拓扑排序。
何谓“拓扑排序”?
对有向图进行如下操作:
按照有向图给出的次序关系,将图中顶点排成一个线性序列,对于有向图中没有限定次序关系的顶点,则可以人为加上任意的次序关系。
例如:对于下列有向图
B
D
A
C
可求得拓扑有序序列:
A B C D 或 A C B D
由此所得顶点的线性序列称之为拓扑有序序列
B
D
A
C
反之,对于下列有向图
不能求得它的拓扑有序序列。
因为图中存在一个回路 {B, C, D}
如何进行拓扑排序?
一、从有向图中选取一个没有前驱
的顶点,并输出之;
重复上述两步,直至图空,或者图不空但找不到无前驱的顶点为止。
二、从有向图中删去此顶点以及所
有以它为尾的弧;
a
b
c
g
h
d
f
e
a
b
h
c
d
g
f
e
在算法中需要用定量的描述替代定性的概念
没有前驱的顶点 入度为零的顶点
删除顶点及以它为尾的弧 弧头顶点的入度减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 关键路径
问题:
假设以有向网表示一个施工流图,弧上的权值表示完成该项子工程所需时间。
问:哪些子工程项是“关键工程”?
即:哪些子工程项将影响整个工程的完成期限的。
整个工程完成的时间为:从有向图的源点到汇点的最长路径。
a
b
c
d
e
f
g
h
k
6
4
5
2
1
1
8
7
2
4
4
例如:
“关键活动”指的是:该弧上的权值增加 将使有向图上的最长路径的长度增加。
源点
汇点
6
1
7
4
如何求关键活动?
“事件(顶点)” 的 最早发生时间 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()}
a
b
c
d
e
f
g
h
k
6
4
5
2
1
1
8
7
2
4
4
0
0
0
0
0
0
0
0
0
6
4
5
7
11
5
7
15
14
18
18
18
18
18
18
18
18
18
18
16
14
8
6
6
10
8
0
7
拓扑有序序列: a - d - f - c - b - e - h - g - k
0
6
4
5
7
7
15
14
18
18
14
16
10
7
8
6
6
0
0
0
0
6
4
5
7
7
7
15
14
14
16
0
2
3
6
6
8
8
7
10
算法的实现要点:
显然,求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 一个无向图G
4. 假设图的顶点是A、B……请根据下面的邻接矩阵画出相应的无向图或有向图。
(a)
(b)
图6.26 一个无向图G
5. 分别给出图6.26所示G图的深度优先搜索和广度优先搜索得到的顶点访问序列。
图6.27 一个带权连通图G
6. 应用prim算法求图6.27所示带权连通图的最小生成树。
图6.28 一个有向图G
7. 写出图6.28所示有向图的拓朴排序序列。
二、算法设计题
1. 如图6.29所示图G,试给出其对应的邻接表,并写出深度优先算法。
2. 如图6.29所示图G,试给出其对应的邻接矩阵,并写出广度优先算法。
3. 编写一个函数通过与用户交互建立一个有向图的邻接表。
4. 编写一个无向图的邻接矩阵转换成邻接表的算法。
图6.29 一个无向图G
5. 已知一个有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)

{ 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)

{
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.近三年科研成果及获奖情况:
具有代表性的论 文、专著和获奖项目 序号 成果(论文、专著获奖项目)名称 成果鉴定、颁奖部门及奖励类别、等级或发表刊物与出版单位、时间 本人署名次序
1
2
3
4
5
6
7
3、目前主持或承担的科研课题
课 题 名 称 下达课题单位 是否主持 经费额度
4、近三年指导研究生情况
年 度 硕 士
2001年
2002年
2003年
学位评定分委员会(学位工作小组)审核意见: 主席(组长)签字: 年 月 日
同课章节目录