1、 《计算机软件基础》多媒体教程 第十一讲 第四章 数据结构 4.1 基本概念 4.1.0 数据结构的研究内容 ※ 数据(对象)的性质 什么样的数据/集合 ※ 数据联系 相互之间是什么关系 ※ 数据表示 如何在计算机中存储 ※ 数据处理 如何对数据进行操作 4.1.1 数据结构的定义 ※ 数据结构的定义 设B是一个二元组,记为B=(K,R)。K是数据的有限集合,R是K中各数据的有限关系集合,则称B是数据结构。 其中,K的成员k1, k2, …,又称为数据元素(或数据结点,结点),记为K={k1, k2, …, kn }。 R的成员r1, r2
2、 …,称为关系,记为R={r1, r2, …, rm| ri= (ks, kt), ks,kt∈K},表示在结点ks, kt之间存在关系ri。 【例5-1.1】数据结构示例。 0~9可组成集合K,在集合K={0~9}上可存在GT(大于)和LT(小于)两种关系。 定义:B=(K, R), K={0, 1, …, 9}, R={GT, LT}, 其中,GT = {(1, 0), (2, 0), (2, 1), …}, LT = {(0, 1), (0, 2), (1, 2), …}, 则称B=(K, R)是一种数据结构。 4.1.2 结点定义 设B=(K, R)是一种数据
3、结构,可有以下定义: ※ 结点的存储单元 结点k的存储单元由结点地址ak,数据场dk和指针场pk组成,记为: k = {ak, dk, pk} ※ 结点的数据场 若结点k的数据场dk有m个分量,即每个结点都有m个不同的数据,则记为: dk = {dik | i=1, ..., m} = {d1k, d2k, …, dmk} ※ 结点的指针场 若结点k的指针场pk有h个分量,每个指针场分量都对应于R的一个关系rj(j=1, …, h),则记为: pk = {pjk | j=1, ..., h} = {p1k, p2k, … , phk} 结点k的存储
4、单元 结点k的数据场 结点k的指针场 假定对应于一种关系rj的指针场pjk,需要指向t(j)个结点,即: pjk = {pjkk | k=1, …, t(j)} = {pj1k, …, pj t(j)k}, 则pk又可以进一步记为 pk = {p11k, … , p1 t(1)k, p21k, … , p2 t(2)k, … , ph1k, … , ph t(h)k} 从而 k = {ak, d1k, d2k,…,dmk, p11k,…, p1 t(1)k, p21k,…, p2 t(2)k,…, ph1k,…, ph t(h)k} ⊙ 空置的指针场 用φ或
5、NULL表示空置的指针场pik,记为pik=φ或pik=NULL。 ※ 结点性质 ⊙ 前件和后件 设r = (k, k’),其中r∈R, (k, k’∈K),则称k是k’关于关系r的前件,k’是k关于关系r的后件。图中用箭头从k指向k’。 当R中只有一种关系r时,可以省略关系的名称,并直接称k是k’的前件,k’是k的后件。 ⊙ 孤立结点 若在K中对于关系r而言,既无前件又无后件的k称为孤立结点。 对于非孤立的结点,定义: ⊙ 始结点 若在K中不存在任何k’,使得成立(k’,k)∈r, 则称k为r的始结点。 ⊙ 终结点 若在K中不存在任何k’,使得成立(k,
6、k’)∈r, 则称k为r的终结点。 ⊙ 内结点 既非始结点又非终结点的称为r的内结点。 【例4-1.2】结点性质示例 定义B1=(K,R),其中 K={1, 2, 3, 4, 5, 6, 7}, R = {N,T}, N = {(1,2), (1,4), (2,3), (2,6), (3,7), (6,3), (7,3)}, T = {(2,6), (4,5), (5,2), (6,7)}。 结 关系 N 关系 T 点 前件 后件 前件 后件 1 2,4 2 1 3,6 5 6 3
7、 2,6,7 7 4 1 5 5 4 2 6 2 3 2 7 7 3 3 6 关系N 关系T 始结点 1 4 终结点 4 7 内结点 2,3,6,7 2,5,6 孤立结点 5 1,3 4.1.3 数据结构的存储方式 假定r是B=(K, R)中的一种关系,实现r的存储方式有以下两种: ※ 顺序存储 若对于任何一对结点(k, k’)都成立ak’ = ak + s(s为存储单元的大小,固定长度),则称r是用顺序方式存储的, 简称顺序存储。 ※ 链接存储 若对于任何一对结点(k, k’
8、)都成立ak’∈pk,则称r是用链接方式存储的,简称链接存储。 例如,在N中各结点的最多后件数大于1,则只能用链接存储。 在T中,各结点的最多后件数不超过1,则既可用顺序存储,也可用链接存储。 【例4-1.3】存储单元的图形表示。 在数据结构B1=(K, R)中,每个结点k的数据场只有一个分量dk。 由于所有结点关于关系N的后件最多有2个,关于关系T的后件均为1个,则k的存储单元为: k = {ak, dk, (pN1k, pN2k), pT}。 现以链接方式存储关系N,以顺序方式存储关系T, B1=(K, R)的存储单元如图所示(虚设存储单位的地址)。
9、4.1.4 数据结构的分类 根据结点的相互关系来分类 (1) 纯集合:结点间相互无关系,全部为孤立结点。 (2) 线性:只有一个始结点和一个终结点,各结点的前件和后件数至多一个,无孤立结点。 (3) 树状:只有一个始结点,各结点的前件数至多一个,无孤立结点。 (4) 网状:始结点和终结点的数目任意,各结点的前件和后件数任意,无孤立结点。 4.1.5 基本操作 数据结构的基本操作包括查找,增添,删除和排序。 (1) 查找:根据地址、序号或某个数据场的值来寻找某个结点。 例如,查找结点值为key=6的结点,即查找ki==6,得i=5。 (2) 增添
10、根据某种关系,在保持数据结构的特点不变情况下,增加结点或结点组。 例如,在已有的数据结构中增添两个结点: (3) 删除:在保持数据结构的特点不变情况下,删去某些结点或结点组。 例如,在已有的数据结构中删除两个结点: (4) 排序:按某种方式使各结点有序排列。 例如,使数据结构中的结点按照从小到大排序: 4.1.6 数据结构的学习内容 ※ 数据类型: 线性,树状,网状。 ※ 存储方式: 顺序存储,链接存储。 ※ 基本操作: 查找,增添,删除和排序。 ※ 算法实现: 用C语言编程。 《计算机软件基础》多媒体教程 第四章 数
11、据结构 4.2 线性表 线性表(list),又称并列表,线性并列表,或者有序表。 4.2.1 基本问题 ※ 线性表的定义 设B=(K, R), 若K中有n个结点K={k0, …, kn-1},R中只有一种关系N,即 N={(ki, ki+1) | ki, ki+1∈K, 0 ≤ i ≤ n-1}, 则称B为线性表。 ※ 推论 有且仅有一个始结点和一个终结点,其余为内结点 除终结点外,每个结点有且仅有一个后件。 除始结点外,每个结点有且仅有一个前件。 ※ 课程约定 ⊙ 如果没有特别说明,数据结构中的结点k只有一个数据场分量。 ⊙ 假定可以调用一个已
12、设计的出错函数error(), 用于出错处理。 ※ 线性表的顺序存储 采用数组实现线性表的顺序存储。 ⊙ 存储单元 ki = {aki, dki} 可省略指针场,其中, aki = i dki = a[i], pki = aki+1 = aki + 1 = i + 1 ⊙ 数据定义和生成线性表 #define M 1000 short n; /* M>=n */ short a[M]; /* 在a[0]~a[n-1]中分别存放各结点的值dk0~ dkn-1 */ ⊙ 生成线性表的程序 scanf("%hd", &
13、n);
if(n > M || n < 1)
error(); /* 结点数超界 */
for(j=0; j 14、short n;
⊙ 生成带哨兵的链表
指针Head指向一个空置的结点(哨兵),首指针为Head->next ,即Head->next为始结点k0的地址ak0,终结点kn-1的指针next指向空(NULL)。
NODE *Head; /* 设置哨兵 */
/* 生成哨兵 */
if((Head = (NODE *)(malloc(sizeof(NODE))) == NULL)
error(); /* 内存分配失败 */
Head->next = NULL;
/* 生成链表 */
createList(Head);
15、
⊙ 链表生成函数
void createList(NODE *head)
{
short n, j, num;
NODE *node;
scanf("%hd", &n);
if(n < 1)
error(); /* 结点数超界 */
for(j=0; j 16、m;
node->next = head->next;
head->next = node;
}
}
4.2.2 查找结点
※ 查找结点的操作
⊙ 按结点值查找
根据结点kj的值dkj查找结点,打印序号j,使得dkj==key。
⊙ 按结点地址查找
根据结点kj的序号j(ak),查找结点kj,打印结点kj的值dkj。
⊙ 按前件或后件查找
根据结点kj的前件(或后件)k’的值dk’,打印序号j和结点kj的值dkj,使得dk’==key。
※ 顺序存储的查找结点
⊙ 根据结点值查找结点的程序
#include 17、h>
#include 18、
scanf(“%hd”, &n);
if(n > M || n < 1)
error(); /* 结点数超界 */
for(j=0; j 19、rtNode(a, n, j)
short n, a[ ], j;
{
if(j<0 || j>n-1)
error(); /* 结点地址超界 */
printf(“node(%hd) = %hd\n”, j, a[j]);
}
main()
{
short n, a[M], j;
scanf(“%hd”, &n);
if(n > M || n < 1)
error(); /* 结点数超界 */
for(j=0; j 20、
prtNode(a, n, j);
}
⊙ 算法复杂度
查找次数与n无关,是一个常数C,称该算法的时间复杂度为O(C)。
※ 链接存储的查找结点
main()
{
NODE *Head, *node;
short key;
/* 生成哨兵 */
if((Head = (NODE *)(malloc(sizeof(NODE))) == NULL)
error(); /*内存分配失败 */
Head->next = NULL;
/* 生成链表 */
createList(Head);
/* 读 21、入key */
scanf("%hd", &key);
/* 查找结点 */
if(node = searchNode(Head, key))
printf("node = %hd\n", node->num);
else
error(); /* 查找结点失败 */
}
⊙ 查找结点函数
NODE* searchNode(NODE *head, short key)
{
NODE *node;
for(node=head->next; node; node=node->next)
if(node->num= 22、key)
return(node);
return(NULL); /* 查找结点失败 */
}
⊙ 算法复杂度
查找次数与n成正比,最多为n次。称该算法的时间复杂度为O(n)。
4.2.3 添加结点
※ 顺序存储的添加结点
⊙ 添加结点的操作
已知结点kj的地址akj,在kj后添加新结点k’,使得dk’=key。
将a[n-1]到a[j+1]向数组尾部移动,腾出添加新结点key的位置,再将新结点key插入。
⊙ 顺序存储添加结点的程序
#include 23、 1000
void addNode(a, n, j, key)
short a[], j, key;
short *n; /* 添加结点后*n要加1 */
{
short k;
if(j<-1 || j>(*n)-1)
error(); /* 结点地址超界 */
for(k=(*n)-1; k>j; k--)
a[k+1] = a[k]; /* 向后移动 */
a[j+1] = key;
(*n)++;
}
main()
{
short a[M], n, j, key;
scanf(“%hd”, 24、 &n);
if(n > M-1 || n < 1)
error(); /* 结点数超界 */
for(j=0; j 25、node后面插入结点k’。
⊙ 链接存储添加结点的函数
void addNode(NODE *node, short key)
{
NODE *key;
if (node==NULL)
error(); /* 非法结点 */
key =(NODE*)(malloc(sizeof(NODE)));
if(key == NULL)
error(); /* 内存分配失败 */
key->num = key;
key->next = node->next;
node->next = key;
}
⊙ 链接存储添 26、加结点的算法复杂度
添加结点的操作次数与n无关,是一个常数C,算法复杂度为O(C)。
⊙ 链接存储添加结点的程序
#include 27、
main()
{
short key, nxtkey;
NODE *Head, node;
/* 生成哨兵 */
if( (Head = (NODE *)(malloc(sizeof(NODE))) == NULL)
error(); /* 内存分配失败 */
Head->next = NULL;
/* 生成链表 */
createList(Head);
/* 读入key, nxtkey */
scanf("%hd%hd", &key, &nxtkey);
/* 查找结点 */
28、 if(!(node = searchNode(Head, key))
error(); /* 查找结点失败 */
/* 添加结点 */
addNode(node, nxtkey);
}
4.2.4 删除结点
※ 顺序存储的删除结点
⊙ 删除结点的操作
已知结点kj的地址akj,删除kj。
将a[j+1]到a[n-1]向数组前部移动,待删结点a[j](kj)被覆盖。
⊙ 顺序存储删除结点的程序
#include 29、leteNode(a, n, j)
short a[], j;
short *n; /* 删除结点后*n要减1 */
{
short k;
if(j<0 || j>(*n)-1)
error(); /* 结点地址超界 */
for(k=j+1; k<(*n); k++)
a[k-1] = a[k]; /* 向前移动 */
(*n)--;
}
main()
{
short a[M], n, j, key;
scanf(“%hd”, &n);
if(n > M-1 || n < 1)
error(); 30、 /* 结点数超界 */
for(j=0; j 31、 *node)
{
NODE *key;
if (node==NULL)
error(); /* 非法结点 */
key = node->next;
node->next = key->next;
free(key); /* 释放删除结点的存储单元 */
}
⊙ 链接存储删除结点的算法复杂度
删除结点的操作次数与n无关,是一个常数C,算法复杂度为O(C)。
⊙ 链接存储删除结点的程序
#include 32、DE
{
short num;
NODE *next;
};
void createList(NODE *head)
{ ...... } /* 略 */
NODE *searchNode(NODE *head, short key)
{ ...... } /* 略 */
main()
{
short key;
NODE *Head, node;
/* 生成哨兵 */
Head = (NODE *)(malloc(sizeof(NODE)));
if(Head == NULL)
33、 error(); /* 内存分配失败 */
Head->next = NULL;
/* 生成链表 */
createList(Head);
/* 读入key */
scanf("%hd", &key);
/* 查找结点 */
if(!(node= searchNode(Head, key))
error(); /*查找结点失败 */
/* 删除结点后件 */
deleteNode(node);
}
4.2.5 排序
※ 排序问题
以升序为例讨论排序问题。
⊙ 定义: 34、
已知结点序列K = {k0, k1, k2, …, kn-1},经排序得到K' = {k’0, k’1, k’2,…, k’n-1},使得K和K’中结点数值相同,而dki’ ≤ dk’i+1(i=0, 1, …, n-2),称新的结点序列K’为已排序的结点序列。
⊙ 需要掌握五种排序算法:
(1) 插入排序法 以下仅讨论插入排序算法
(2) 选择排序法
(3) 冒泡排序法
(4) 快速排序法
(5) 合并排序法
※ 插入排序算法
⊙ 插入排序算法描述
设已知结点序列为K = {k0, k1, k2, …, kn-1},
将已排序的结点序列记为S,未排序 35、的结点序列记为U。
初始:令S = {k0},U = K - {k0} = {k1, k2, …, kn-1}
循环执行的插入步骤:
从i=1到n-1
令key = ki | ki∈U,U = U - {ki}
在S中插入ki,使得S = S∪{key}为已排序。
若U=φ,插入排序结束。
⊙ 排序过程示例
已知结点序列为{6, 5, 9, 4, 8}。
初始:令S = {k0} = {6},U = {k1, …, k4} = {5, 9, 4, 8}。
循环插入的过程示例:
Step 1: i=1,插入5, 得S={5, 6}, U 36、{9, 4, 8};
Step 2: i=2,插入9, 得S={5, 6, 9}, U={4, 8};
Step 3: i=3,插入4, 得S={4, 5, 6, 9}, U={8};
Step 4: i=4,插入8, 得S={4, 5, 6, 8, 9}, U=φ,结束。
※ 顺序存储的插入排序函数
void insert(short *a, short n)
{
short key, j, k;
for(j=1; j 37、 for(key=a[j], k=j-1; key=0; k--)
a[k+1] = a[k]; /* 移动 */
a[++k] =key; /* 插入 */
}
}
⊙ 顺序存储的插入排序程序
#include 38、
scanf(“%hd”, &n);
if(n > M-1 || n < 1)
error(); /* 结点数超界 */
for(j=0; j 39、 ≈ n2 / 2 ≈ n2
所以,算法复杂度为O(n2)。
※ 链接存储的插入排序
⊙ 链接存储的插入排序算法
1) 初始设置
由head->next指向S = {k0},由指针unsort指向未排序的结点序列U=K-S=K-{k0}。
程序语句为:
unsort = head->next->next;
head->next->next = NULL;
2) 从U中逐个取出结点在S中排序的过程
while(unsort)
从U中取出一个结点,由指针key指向,即在U中删除结点:
key = ki | ki∈U,U = 40、U - {ki}
程序语句为:
key = unsort;
unsort = key->next;
将结点key插入S:S = S∪{key},并且保持S为已排序。
⊙ 链接存储的插入排序函数
void insert(head)
NODE *head;
{
NODE *unsort, *key, *node;
if(head->next==NULL || head->next->next==NULL)
return; /* 空链表或者单结点链表 */
unsort = head->next->n 41、ext; /* 初始:S = {k0},U = K - {k0} */
head->next->next = NULL;
while(unsort) /* U非空,进行循环 */
{
key = unsort; /* U = U - {key} */
unsort = key->next;
for(node=head; node->next; node=node->next)
if(key->number <= node->next->number)
break; /* 定位插入点 42、 */
key->next = node->next; /* 将key插入S,保持S有序 */
node->next = key;
}
}
⊙ 链接存储的插入排序算法的时间复杂度
从U取出结点为n-1次(while循环),定位插入点的次数最多为n-1(for循环),所以算法复杂度为O(n2)。
⊙ 链接存储的插入排序程序
#include 43、void createList(NODE *head)
{ ...... } /* 略 */
void insert(NODE *head)
{ ...... } /* 略 */
main()
{
NODE *Head, node;
/* 生成哨兵 */
if(!(Head=(NODE *)(malloc(sizeof(NODE))))
error(); /* 内存分配失败 */
Head->next = NULL;
createList(Head); /* 生成链表 */
insert(Head); /* 44、 排序 */
for(node=Head->next; node;
node=node->next) /* 打印 */
printf(“%hd\n”, node->num);
}
4.2.6 存储方式与算法复杂度
以结点数n为基数,各种算法的时间复杂度O(f(n))表征所需的最多时间,并且与存储方式相关(见下表)。可以发现,两种存储方式没有绝对的优劣。例如,prtNode函数用顺序存储的算法复杂度与n无关,比链接存储要快。而添加结点和删除结点的函数,用链接存储的算法复杂度为O(C),优于顺序存储的O(n)。
编程时要根据实际情况来决定采用哪一种存 45、储方式。例如,需要频繁进行添加结点和删除结点操作的应用,采用链接存储较好。而只需要根据结点地址打印结点,则采用顺序存储较好。
另外,还应该综合n的可知情况决定采用数组还是链表(见3.3.5数组和指针应用的讨论)。
基本操作
操作函数
顺序存储
链接存储
查找结点
searchNode()
O(n)
O(n)
查找结点
prtNode()
O(C)
O(n)
添加结点
addNode()
O(n)
O(C)
删除结点
deleteNode()
O(n)
O(C)
插入排序
insert()
O(n2)
O(n2)
作 业
※ 习题 4-1(数据结构),4-38(存储单元的图形表示)
※ 线性表上机题:4-39.1(顺序表操作),4-40.1(链接表操作),
4-39.2或者4-39.3任选1题,4-40.2或者4-40.3任选1题
将上机题发给助教。






