03 习题1.9 有序数组的插入《PTA浙大版《数据结构(第2版)》题目集》
1.原题链接
2.题目描述
本题要求将任一给定元素插入从大到小排好序的数组中合适的位置,以保持结果依然有序。
函数接口定义:
bool Insert( List L, ElementType X );
其中List
结构定义如下:
typedef int Position;
typedef struct LNode *List;
struct LNode {
ElementType Data[MAXSIZE];
Position Last; /* 保存线性表中最后一个元素的位置 */
};
L
是用户传入的一个线性表,其中ElementType
元素可以通过>、==、<进行比较,并且题目保证传入的数据是递减有序的。函数Insert
要将X
插入Data[]
中合适的位置,以保持结果依然有序(注意:元素从下标0开始存储)。但如果X
已经在Data[]
中了,就不要插入,返回失败的标记false
;如果插入成功,则返回true
。另外,因为Data[]
中最多只能存MAXSIZE
个元素,所以如果插入新元素之前已经满了,也不要插入,而是返回失败的标记false
。
裁判测试程序样例:
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 10
typedef enum {false, true} bool;
typedef int ElementType;
typedef int Position;
typedef struct LNode *List;
struct LNode {
ElementType Data[MAXSIZE];
Position Last; /* 保存线性表中最后一个元素的位置 */
};
List ReadInput(); /* 裁判实现,细节不表。元素从下标0开始存储 */
void PrintList( List L ); /* 裁判实现,细节不表 */
bool Insert( List L, ElementType X );
int main()
{
List L;
ElementType X;
L = ReadInput();
scanf("%d", &X);
if ( Insert( L, X ) == false )
printf("Insertion failed.\n");
PrintList( L );
return 0;
}
/* 你的代码将被嵌在这里 */
输入样例1:
5
35 12 8 7 3
10
输出样例1:
35 12 10 8 7 3
Last = 5
输入样例2:
6
35 12 10 8 7 3
8
输出样例2:
Insertion failed.
35 12 10 8 7 3
Last = 5
3.参考答案
二分查找
Position BinarySearch( List L, ElementType X ){
Position Left, Right, Mid;
Left = 0;
Right = L->Last;
while( Left <= Right )
{
Mid = (Left + Right) / 2;
if( L->Data[Mid] < X )
Right = Mid - 1;
else if( L->Data[Mid] > X )
Left = Mid + 1;
else
return Mid;
}
return Left;
}
bool Insert( List L, ElementType X ){
Position P, i;
if (L->Last == (MAXSIZE-1))
return false;
P = BinarySearch( L, X );
if ( L->Data[P] == X )
return false;
else {
for (i=L->Last; i>=P; i--)
L->Data[i+1] = L->Data[i];
L->Data[P] = X;
L->Last++;
return true;
}
}
4.解题思路
第一种算法
遍历
如果没有学习过二分查找,那么最容易想到的算法是从数组的第一个元素开始遍历。
因为原数组元素是降序排列的,如果X
插入在第一个位置或者中间某个位置,只需要从左至右找到第一个比X
小的元素,然后把这个元素及其后面的元素向后挪一个位置,即可插入X
。如果遍历完了找到的元素都比X
大,那么把X
插入在数组最后即可。
因为依次查找每一个元素,所以时间复杂度为 O ( N ) O(N) O(N)。
第二种算法
二分查找
上一个题已经学过了二分查找,二分查找的时间复杂度是 O ( log N ) O(\log N) O(logN),是比遍历更好的算法。
二分查找可以指定的待查找元素,在查找的过程中,左右边界会不断地向待查找值X
逼近,如果没有找到指定的待查找元素X
,那么退出循环时,定义的左边界Left
会大于定义的右边界Right
,此时左边界的值刚好大于待查找的值,左边界的位置就是X
应被插入的位置。
例如,如下表所示的数组,如果待插入值X
为10,那么X
应该插入在数组下标3
的位置。
二分查找左边界Left
从数组下标0
开始增加,右边界Right
从数组下标8
开始减小,退出循环时左边界Left
为数组下标3
,右边界Right
为数组下标2
,X
应该插入的位置为左边界Left
的位置。
数组下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|
数组元素 | 17 | 13 | 11 | 9 | 8 | 7 | 5 | 4 | 2 |
第一次循环 | Left |
mid |
Right |
||||||
第二次循环 | Left |
mid |
Right |
||||||
第三次循环 | Left(mid) |
Right |
|||||||
第四次循环 | Right(Left、mid) |
||||||||
退出循环 | Right |
Left |
5.答案详解
答案一
遍历
bool Insert( List L, ElementType X ){
bool insert;
//如果线性表满了,返回false
if (L->Last+1 == MAXSIZE)
insert = false;
else{
//从数组下标0开始遍历
for (int i = 0; i <= L->Last; i++){
//如果找到了X则不用插入,返回false
if (X == L->Data[i]){
insert = false;
break;
}
//如果找到了第一个比X更小的元素
if (X > L->Data[i]){
//将比X小的元素都是向后挪动一个位置
for (int j = L->Last+1; j > i; j--)
L->Data[j] = L->Data[j-1];
//插入X
L->Data[i] = X;
//表长度加1,结束循环
L->Last++;
insert = true;
break;
}
//如果遍历结束找到的元素都比X大,那么把X插入在数组最后即可。
if (i == L->Last && X < L->Data[i]){
L->Data[L->Last+1] = X;
L->Last++;
insert = true;
break;
}
}
}
return insert;
}
答案二
二分查找
/* 在顺序存储的表L中查找关键字为X的数据元素 */
Position BinarySearch( List L, ElementType X ){
Position Left, Right, Mid;
Left = 0; /* 初始左边界下标值 */
Right = L->Last; /* 初始右边界下标值 */
while( Left <= Right ){
Mid = (Left + Right) / 2; /* 计算中间元素坐标 */
if( L->Data[Mid] < X )
Right = Mid - 1; /* 调整右边界 */
else if( L->Data[Mid] > X )
Left = Mid + 1; /* 调整左边界 */
else /* L->Data[Mid] == X */
return Mid; /* 查找成功,返回数据元素的下标 */
}
return Left; /* 返回X应被插入的位置 */
}
bool Insert( List L, ElementType X ){
Position P, i;
//如果线性表满了,返回false
if (L->Last == (MAXSIZE-1))
return false;
//二分查找
P = BinarySearch( L, X );
//如果返回的位置对应元素是X,则不需要插入
if ( L->Data[P] == X )
return false;
//否则将返回位置及其后面的元素都是向后挪动一个位置,插入X
else {
for (i=L->Last; i>=P; i--)
L->Data[i+1] = L->Data[i];
L->Data[P] = X;
L->Last++;
return true;
}
}
6.知识拓展
以下代码补全了List ReadInput()
和void PrintList( List L )
的具体实现方式,以便读者在本地IDE调试运行程序。
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 10
typedef enum {true, false} bool;
typedef int ElementType;
typedef int Position;
typedef struct LNode *List;
struct LNode {
ElementType Data[MAXSIZE];
Position Last; /* 保存线性表中最后一个元素的位置 */
};
//补全题目描述中省略的List ReadInput()
List ReadInput(){
List L;
int N;
L = (List)malloc(sizeof(struct LNode));
scanf("%d", &N);
for (L->Last=0; L->Last<N; L->Last++)
scanf("%d", &L->Data[L->Last]);
L->Last--;
return L;
}
//补全题目描述中省略的void PrintList( List L )
void PrintList( List L ){
int i;
printf("%d", L->Data[0]);
for (i=1; i<=L->Last; i++) printf(" %d", L->Data[i]);
printf("\n");
printf("Last = %d\n", L->Last);
}
bool Insert( List L, ElementType X );
int main(){
List L;
ElementType X;
L = ReadInput();
scanf("%d", &X);
if ( Insert( L, X ) == false )
printf("Insertion failed.\n");
PrintList( L );
return 0;
}
/* 你的代码将被嵌在这里 */
Position BinarySearch( List L, ElementType X ){
Position Left, Right, Mid;
Left = 0;
Right = L->Last;
while( Left <= Right ){
Mid = (Left + Right) / 2;
if( L->Data[Mid] < X )
Right = Mid - 1;
else if( L->Data[Mid] > X )
Left = Mid + 1;
else
return Mid;
}
return Left;
}
bool Insert( List L, ElementType X ){
Position P, i;
if (L->Last == (MAXSIZE-1))
return false;
P = BinarySearch( L, X );
if ( L->Data[P] == X )
return false;
else {
for (i=L->Last; i>=P; i--)
L->Data[i+1] = L->Data[i];
L->Data[P] = X;
L->Last++;
return true;
}
}
本题题目描述中引入了数据结构中线性表的概念,线性表是一种抽象数据类型,那么什么是抽象数据类型呢?
抽象数据类型
复杂的程序分割为一些模块 (module)
来实现有许多好处。
- 每个模块是一个逻辑单位并执行某个特定的任务,它通过调用其他模块而使本身保持很小。
- 调试小程序比调试大程序要容易得多。
- 多个人同时对一个模块化程序编程要更容易。
- 一个写得好的模块化程序把某些依赖关系只局限在一个例程中(例程是某个系统对外提供的功能接口或服务的集合),这样使得修改起来更容易。例如,需要以某种格式编写输出,那么重要的当然是让一个例程去实现它。如果打印语句分散在程序各处,那么修改所费的时间就会明显地拖长。
抽象数据类型(Abstract Data Type,ADT)
是一些操作的集合。
- 抽象数据类型是数学的抽象,在ADT的定义中不涉及如何具体实现这些操作,抽象数据类型描述中所涉及的参数不必考虑具体数据类型。在实际应用中,数据元素可能有多种类型,到时可根据具体需要选择使用不同的数据类型。抽象数据类型是模块化程序设计的广泛扩充。
- 例如线性表、集合、图以及它们的操作,它们都可以看作抽象数据类型,就像整数、实数和布尔量是数据类型一样。整数、实数及布尔量有与它们相关的操作,而抽象数据类型也有与之相关的操作。通常这些操作的实现只在程序中编写一次,而程序中任何其他部分需要在该 ADT 上运行其中的一种操作,都可以通过调用适当的函数来进行。如果由于某种原因需要改变操作的细节,通过只修改运行这些 ADT 操作的例程应该可以很容易实现。在理想的情况下,这种改变对于程序的其余部分通常是完全透明的。
线性表的特点和定义
- 由 n ( n ≥ 0 ) n(n≥0) n(n≥0)个数据特性相同的元素构成的有限序列称为线性表。
- 线性表中元素的个数 n ( n ≥ 0 ) n(n≥0) n(n≥0)定义为线性表的长度, n = 0 n=0 n=0 时称为空表。
- 线性表中元素的个数称为线性表的长度。
- 线性表的起始位置称为表头,线性表结束的位置称为表位。
- 对于非空的线性表或线性结构,其特点是:
- 存在唯一的一个被称作「第一个」的数据元素;
- 存在唯一的一个被称作「最后一个」的数据元素;
- 除第一个之外,结构中的每个数据元素均只有一个前驱;
- 除最后一个之外,结构中的每个数据元素均只有一个后继。
- 线性表举例:
- 26 个英文字母的字母表;
- 学生基本信息表,每个学生为一个数据元素,包括学号、姓名、性别、籍贯、专业等数据项;
线性表的类型定义
-
线性表是一个相当灵活的数据结构,其长度可根据需要增长或缩短。
-
对线性表的数据元素不仅可以进行访问,而且可以进行插入和删除等操作。
-
线性表的抽象数据类型定义:
ADT List{ 数据对象:D={ ai|ai∈ElemSet,i=1,2,…,n,n≥0} 数据关系:R={ <ai-1,ai>|ai-1,ai∈D,i=2,…,n} 基本操作: InitList(&L) 操作结果:构造一个空的线性表 L。 DestroyList(&L) 初始条件:线性表 L 已存在。 操作结果:销毁线性表 L。 ClearList(&L) 初始条件:线性表 L 已存在。 操作结果:将 L 重置为空表。 ListEmpty(L) 初始条件:线性表 L 已存在。 操作结果:若 L 为空表,则返回 true,否则返回 false。 ListLength(L) 初始条件:线性表 L 已存在。 操作结果:返回 L 中数据元素个数。 GetElem(L,i,&e) 初始条件:线性表 L 已存在,且 1≤i≤ListLength(L)。 操作结果:用 e 返回 L 中第 i 个数据元素的值。 LocateElem(L,e) 初始条件:线性表 L 已存在。 操作结果:返回 L 中第 1 个值与 e 相同的元素在 L 中的位置。若这样的数据元素不存在,则返回值为 0。 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 无定义。 ListInsert(&L,i,e) 初始条件:线性表 L 已存在,且 1≤i≤ListLength(L)+1。 操作结果:在 L 中第 i 个位置之前插入新的数据元素 e,L 的长度加 1。 ListDelete(&L,i) 初始条件:线性表 L 已存在且非空,且 l≤i≤ListLength(L)。 操作结果:删除 L 的第 i 个数据元素,L 的长度减 1。 TraverseList(L) 初始条件:线性表 L 已存在。 操作结果:对线性表 L 进行遍历,在遍历过程中对 L 的每个结点访问一次。 }ADT List
线性表的顺序存储表示和实现
-
线性表的顺序表示指的是用一组地址连续的存储单元依次存储线性表的数据元素,这种表示也称作线性表的顺序存储结构或顺序映像。
-
这种存储结构的线性表为顺序表(Sequential List)。
-
顺序表逻辑上相邻的数据元素,其物理次序也是相邻的。
-
对顺序表的所有操作都可以使用数组来实现。虽然数组是动态指定的,但还是需要对表的大小的最大值进行估计。通常需要估计得大一些,而这会浪费大量的空间。这是严重的局限,特别是在存在许多末知大小的线性表的情况下。
-
数组实现使得查找指定内容的元素以线性时间执行,而查找第
K
个位置的元素则花费常数时间。然而,插入和删除的花费时间是巨大的。例如,在位置0
插入一个元素,首先需要将整个数组后移一个位置以空出空间来,而删除第一个元素则需要将数组中的所有元素前移一个位置,因此这两种操作的最坏情况为 O ( N ) O(N) O(N)。平均来看, 这两种运算都需要移动表中一半的元素,因此仍然需要线性时间。只通过 N 次相继插入来建立一个表时间复杂度为 O ( N 2 ) O(N^2) O(N2)。因为插入和删除的运行时间非常慢并且表的大小还必须事先已知,所以数组一般不用来实现线性表这种结构,除非程序中最重要的操作是根据存储的顺序查找元素。
为了避免插入和删除的线性开销,线性表可以不连续存储,这种不连续的存储方式的线性表称为链表。