一,数据结构的一般概念
数据:所有能被输入到计算机并被处理的符号的集合
数据元素:数据的基本单位。
数据项:构成数据的不可分割的最小单位,一个数据元素有若干个数据项组成。
数据对象:相同性质的数据元素的集合,是数据的一个子集。
数据类型:一个值的集合和定义在这个集合上的一组操作的总成。
原子类型:值不可在分割的数据类型。
结构类型:值不可在分解成若干的数据类型。
抽象数据类型:抽象数据组织和与之相关的操作。
抽象数据类型(ADT):一个数学模型以及定义在该模型上的一组操作。其定义只与逻辑特性有关,通常采用(数据对象,数据关系,基本操作集)这样的三元来表示抽象数据类型。
数据结构:相互之间存在的一种或多种特定关系的数据元素的集合,包括:逻辑结构,存储结构和数据的运算。
数据的三要素:逻辑结构,物理结构,数据元素。
数据的逻辑结构:数据元素之间的逻辑关系。
数据的逻辑结构分类图:
集合:结构中的数据元素之间除了“同属一个集合”的关系之外,没有任何关系。
线性结构:结构中的数据元素之间只存在一对一的关系。
树形结构:结构中的数据元素之间存在一对多的关系。
图状结构或网状结构:结构中的数据元素之间存在多对多的关系。
数据的存储结构:数据结构在计算机中的表示,也称物理结构。
顺序存储:把逻辑上相邻的元素存储在物理位置上相邻的存储单元里,通过存储单元的邻接关系来表示元素之间的逻辑关系。
优点:实现随机存储,每个元素占用空间小。
缺点:只能使用相邻的一整块存储单元,会产生较多的外部碎片。
链式存储:不要求逻辑上相邻的元素在物理位置上也相邻,通过指针表示元素之间的逻辑关系。
优点:不会出现碎片现象,充分利用所有的存储单元。
缺点:每个元素要占用存储指针,需要多占用部分存储空间,而且只能顺序存取。
索引存储:存储信息的同时,建立附加的索引表,索引表中的每一项成为索引项,索引项的一般形式(关键字,地址)
优点:检索速度快。
缺点:增加索引表,占用较多存储空间,增删数据时也要修改索引表,花费较多的时间。
散列存储:根据元素的关键字直接计算出该元素的存储位置,也称Hash存储
优点:检索,增删节点操作都很快
缺点:散列函数不好可能会出现元素存储单元的冲突,解决冲突会增加时间 ,空间的开销。
算法的基本概念:
算法对特定问题求解步骤的一种描述,它是指令的有限序列,期中每一条指令都表示一个或多个操作。
算法的5个重要性:有穷性,确定性,可行性,输入,输出。
算法设计的要求:正确性,可读性,健壮性,效率与低存储需求。
算法效率的度量:
通常用时间复杂度和空间复杂度来描述。
时间复杂度:算法中所有语句的频度(指该语句在算法中被重复执行的次数)之和记作T(n),时间复杂度主要分析T(n)的数量级。
算法中的基本运算(最深层循环内的语句)的频度与T(n)同数量级,所以一般采用算法中最基本的频度f(n)来分析算法时间复杂的度。即T(n)=O(f(n))
空间复杂度:算法耗费的存储空间,记作S(n)=O(g(n))
算法原地工作指算法所需要辅助空间是常量,即O(1)
二,线性表的定义和基本操作
线性表的定义: 具有相同数据类型的n(N>=0)个数据元素的有限序列。
线性表的特点:
1.除第一个元素外,每个元素有且仅有一个直接前驱,除最后一个元素外,每个元素有且仅有一个直接后继
2.表中元素个数有限
3.表中元素具有逻辑上的顺序关系
4.表中每个元素都是数据元素,每个元素都是单个元素
5.表中元素的数据类型都相同,即每个元素占有相同大小的存储空间
6.表中元素具有抽象性,即只关注与逻辑结构,不关注于元素表示的内容
7.线性表示一种逻辑结构 ,表示元素之间一对一的相邻关系;顺序表和链表是存储结构,表示物理结构
线性表的基本操作:
InitList(&L):初始化表
Length(L):求表长
LocateElem(L,e):按值查找操作
GetElem(L,i):按位查找操作
ListInsert(&L,e):插入操作
ListDelete(&L,i,&e):删除操作
Print List(L):输出操作
Empty(L):判空操作
DestroyList(&L):销毁操作
线性表的顺序表示
顺序表的定义:用一组连续的存储单元,一次存储线性表中的数据元素,使得逻辑上相邻的数据元素在物理位置上也相邻。
顺序表的特点:随机访问,并且存储密度高,但是增删操作需要移动大量元素
结构体描述:
//数组空间静态分配
#define MaxSize 50 //数组最大长的
typedef struct{
ElemType data[MaxSize]; //顺序表的元素
int length; //顺序表当前长度
}sqList ; //静态分配数组顺序表的类型定义
//数组空间动态分配
#define InitSize 100 //表长度的初始定义
typedef struct{
Elemtype *data;
int MaxSize,length
}SeqList;//动态分配数组顺序表的类型定义
基本操作相关代码:
1.插入操作
bool ListInsert(SqLsit &L,int i,ElemTYpe e){
if(i<1 || i>L.length) //判断插入范围是否有效
return false;
if(L.length >= MaxSize){ //判断存储空间是否已满
return false;
for(int j=L.length;j>=i;j--) //将i之后的元素一次后移
L.data[j] = L.data[j-1];
L.data[i-1]= e; //在i位置上赋值,注意i是位号序,i-1是数组下标
L.length++; //长度加一
return ture;
}
//移动点平均此数位n/2,时间复杂度位O(n)
2.删除操作
bool ListDelete(SqList &L,int i,EleType &e){
if(i<0||i>L.length) //判断删除范围是否有效
return false;
for(int j=i;j<L.length;j++) //将i之后的值依次前移
L.data[j-1]=L.data[j];
L.length--; //长度减一
return true;
//移动节点平均次数(n-1)/2,时间复杂度O(n)
3.按值查找
int LocateElem(SqLsit L,ElemtType e){
//实现查找顺序表中的值为e的元素,查找成功则返回元素位置,否则返回0;
int i;
for(i=0;i<L.length;i++){
if(L.data[i]==e)
return i+1; //下标为i的元素值为e,其位置为i+1
}
return 0;
}
//移动节点平均次数为(n+1)/2,时间复杂度为O(n)
线性表的链式表示
单链表的定义:
通过一组任意的存储单元来存储线性表中的数据元素,每个链表节点除了放自身的信息外,还要存放一个指向后继的指针,其中data为数据域。next为指针域。
节点类型定义
typedef struct LNode{
Elemtype data;
struct LNode *next;
}LNode ,*LinkList;
1.单链表中的元素是离散的分布在存储空间中的。所以是非随机存储结构,想找到某个元素必须从头遍历。
2.通常用头指针标识一个单链表,此外,在单链表的第一个结点之前附加一个节点,称之为头节点。头结点中可以不加任何信息,也可以记录表长等。
引入头结点的优点:
开始节点放在头结点的指针域中,所以链表的第一个节点位置上的操作与其他位置上的操作一致,不需要特殊处理。
若链表为空,头指针是指向头结点的非空指针(头结点的指针域为空),所以空表与非空表的处理统一。
单链表解决了顺序表需要大量连续存储空间的缺点,但是单链表附加了指针域,也存在浪费存储空间的缺点。
基本操作相关代码
1.建立单链表
//头插法
LinkList CreatList1(LinkList &L){
//从表尾到表头逆向建立单链表L,每次都在头结点之后插入元素
LNode *s;
int x;
L=(LinkList)malloc(sizeof(LNode));//创建头节点
L->next = NULL;//初始空链表
scanf("%d",&x);//输入节点元素
while(x!=NULL){
s=(LNode*)malloc(sizef(LNode));//创建新的节点
//下面为头插法细节
s->data=s;
s->next=L->next;
L->next=s;
scanf("%d",&x);
}
return L;
}
//尾插法
LinkList Creatlist2(LinkList &L){
//从表头到表尾正向建立单链表L,每次均在表为插入元素
LNode *s,*r=L;//除了s这个新节点的指针,还要建立一个指向尾节点的指针
int x;
L= (LinkList)malloc(sizeof(LNode));
L->next = NULL;
scanf("%d",&x);
while(x!=NULL){
s = (LinkList)malloc(sizeof)(LNode));
//下面为尾插法的细节
s->data = s;
r->next = s;
r=s; //r指向新的尾指针
scanf("%d",&x);
}
r->next=NULL;
return L;
}
//头插法建立单链表,读入数据的顺序与生成的链表中的元素的顺序相反的。尾插法建立的单链表,读入数据的顺序与生成链表中的元素的顺序是相同的。
//两种方法的时间复杂度都为O(n)
2.按序号查找节点值
LNode *GetElem(LinkList L,int i){
int j=1;//计数器
LNode *p=L->next //p指向头结点指针
if(i==0)
return L;
if(i<1)
reutrn NULL;
while(p&&j<i){//从第一个节点开始查找,查找第i-1个节点
p=p->next;
j++;
}
return p; //返回第i个节点的指针,如果大于表长,p=NULL,直接返回p即可
}
//时间复杂度为O(n)
3.按值查找
LNode *LocateElem(LinkList L,Elemtype e){
LNode *p = L->next;
while(p!=NULL && p->data!=e){
p=p->next;
}
return p;
}
//时间复杂度为O(n)
4.插入节点主要代码片段
p=GetElem(L,i-1); //查找插入位置的前驱节点
s->next = p->next;
p->next=s;
5.删除节点操作
//删除当前指针下一个节点
p=GetElem(L,i-1);
q=p->next;
p->next = q->next; free(p);
//删除当前指针所在结点
q=p->next;
p-data = p->next->data;
p-next = q->next; free(q);
双向链表:
双向链表是单链表中有一个指向后继节点的指针next的基础上,增加了一个指向前驱节点的指针prior
//节点类型定义
typedef struct DNode{
ElemType data;
Struct DNode *prior ,*next}DNode ,*LinkList;
//插入操作主要代码
s->next = p->next;
p-next-prior=s;
s->prior=p;
p->next = s;
//删除操作主要代码
p->next = q->next;
q->next->prior=p;
free(p);
循环链表
循环单链表:在单链表的基础上,表中最后一个节点的指针不是NULL,而是改为指向头结点,整个链表形成一个环。
因为没有指针域为NULL的节点,所以,循环单链表的判空条件不是头结点的指针是否为空,而是它是否等于头指针。
插入,删除操作算法与单链表一致,只是在尾部操作不同,
循环双向链表:在双向链表的基础上,表中最后一个节点的指针不是NULL,而是改为指向头结点,整个链表形成一个环。
判空条件为头结点的prior域和next域都等于头结点。
静态链表
静态链表是借助数组来描述线性表的链式存储结构,节点也有数据域data和指针域next,不过这里的指针域指的数组下标(游标)
结点类型定义
#define MaxSize //静态链表的最大长度
typedef struct{
Elemtype data;
int next; //下一个元素的数组下标
}SlinkList[MaxSize];
//同顺序表一样,需要预先分配一块连续的内存空间。
//静态链表一next=-1 作为其结束标志
顺序表和单链表的比较
1.存取方式
顺序表可以顺序存取,也可以随机存取;链表只能顺序存取
2.逻辑结构和物理结构
顺序表,逻辑上相邻的元素,物理位置上也相邻;链表,逻辑上相邻,物理位置上不一定相邻
3.查找,插入和删除操作时间复杂度
按值查找:顺序表无序时,两者时间复杂度都为O(n);当顺序表有序时,可以采用折半查找,时间复杂度O(log2 n)
按位查找:顺序表随机访问,时间复杂度为链表平均时间复杂度为O(n)
三,其他线性结构
栈
栈的基本概念
栈的定义:只允许一端进行插入和删除操作的线性表
栈顶:栈允许进行插入和删除的那一端
栈底:固定的,不允许进行插入和删除的一端。
栈是一个先进后出的线性表
栈的基本操作
- InitStack(&S):栈的初始化
- StackEmpty(S):判断栈是否为空
- Push(&S,x):进栈
- Pop(&S,&x):出栈
- GetTop(S,&x):读栈顶元素
- ClearStack(&S):销毁栈
顺序栈:
#define MaxSize 50 //定义栈中元素的最大个数
typedef struct{
Elemtype data[MaxSize]; //存放栈中元素
int top; //栈顶指针
}SqStack;
- 栈空条件:S.top=-1;栈满条件:S.top=MaxSize-1;栈长:S.top+1
顺序在的基本操作:
//初始化
void InitStack(&S){
S.top=-1; //初始化栈顶指针
}
//判栈空
bool StackEmpty(S){
if(S.top==-1)
return true;
else
return false;
}
//进栈
bool Push(SqStack &S,ElemType x){
if(S.top==MaxSize-1) //栈满
return false;
S.data[++S.top]=x; //指针自增1,入栈
return true;
}
//出栈
bool Pop(SqStack &S,ElemType &x){
if(S.top==-1) //栈空
return false;
x=S.data[S.top--]; //出栈,指针减一
return true;
}
//读栈顶元素
bool GetTop(SqStack S,ElemType &x){
if(S.top==-1)
return false;
x=S.data[S.top];
return true;}
- 共享栈
利用栈底位置不变的特性,让两个栈共享一个一维数组空间,将两个栈的栈底分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸
- top0=-1是0号栈为空,top1=MaxSize时1号栈为空
- 仅当两个栈顶指针相邻时,判断为栈满
链栈
采用链式存储的栈,通常采用单链表实现,并规定所有操作在单链表的表头进行,且链栈没有头结点,Lhead指向栈顶元素
结构体描述
typedef struct Linknode{
ElemType data;
struct Linknode *next;
}*LiStack;
采用链式存储,便于插入、删除操作,具体步骤与单链表类似。
队列
队列的基本概念
队列的定义:只允许在表的一段进行插入,表的另一端进行删除
- 队头:允许删除的一端
- 队尾:允许插入的一端
- 空队列:不含任何元素的空表
- 队列是一个先进先出的线性表
队列的基本操作
- InitQueue(&Q):初始化队列
- QueueEmpty(Q):判队列空
- EnQueue(&Q):入队
- DeQueue(&Q,&x):出队
- GetHead(Q,&x):读队头元素
队列的顺序存储结构
队列的顺序存储
分配一块连续的存储单元存放队列中的元素,并设置两个指针front和rear分别指示队头元素和队尾元素的位置。
结构体描述
#define MaxSize 50
typedef struct{
ElemType data[MaxSize];
int front,rear;
}SqQueue;
- 队头指针指向对头元素,队尾指针指向队尾元素的下一个位置
- 队列判空条件:Q.front=Q.rear=0
- 队列会出现假溢出的情况
循环队列
将顺序队列想象成一个环状空间,也就是逻辑上将存储队列元素的表看成一个环,即循环队列
- 初始时:Q.front=Q.rear=0
- 对首指针进1:Q.front=(Q.front+1)%MaxSize
- 对尾指针进1:Q.rear=(Q.rear+1)%MaxSize
- 队列长度:(Q.rear+MaxSize-Q.front)%MaxSize
- 队满判断:一般有三种
- 牺牲一个单元来区分队空队满,即队头指针在队尾指针的下一位置作为队满的标志
- 队满条件:(Q.rear+1)%MaxSize==Q.front
- 队空条件:Q.front==Q.front
- 队列中元素的个数:(Q.rear-Q.front+MaxSize)%MaxSize
- 类型中增设表示元素个数的数据成员size,则队空条件为Q.size=0,队满条件为Q.size=MaxSize,两种情况中都有Q.front=Q.rear
- 类型中增设tag数据成员,区分队空还是队满。则tag等于0的情况下,因删除导致Q.front==Q.rear则为队空,tag=1的情况下,因插入导致Q.front==Q.rear则为队满
- 牺牲一个单元来区分队空队满,即队头指针在队尾指针的下一位置作为队满的标志
- 循环队列的基本操作相关代码
/初始化
void InitQueue(&Q){
Q.front=Q.rear=0;
}
//判队空
bool isEmpty(Q){
if(Q.rear==Q.front)
return true;
else
return false;
}
//入队
bool EnQueue(SqQueue &Q,ElemType x){
if((Q.rear+1)%MaxSize==Q.front) //队满
return false;
Q.data[Q.rear]=x;
Q.rear=(Q.rear+1)%MaxSize; //队尾指针加一取模
return true;
}
//出队
bool DeQueue(SqQueue &Q,ElemType &x){
if(Q.rear==Q.front)
return false;
x=Q.data[Q.front];
Q.front=(Q.front+1)%MaxSize; //队头指针加一取模
return true;
}
队列的链式存储结构
队列的链式表示即链队列
- 结构体描述
-
typedef struct{ //链式队列结点 ElemType data; struct LinkNode *next; }LinkNode; typedef struct{ //链式队列 LinkNode *front,*rear; //队列的队头和队尾指针 }LinkQueue;
- Q.front==NULL和Q.rear==NULL时,队列为空
- 链队列通常设计成带头结点的单链表,这样插入删除操作统一
- 链队列基本操作相关代码
-
//初始化 void InitQueue(LinkQueue &Q){ Q.front=Q.rear=(LinkNode*)malloc(sizeof(LinkNode)); Q.front->next=NULL; } //判队空 bool IsEmpty(LinkQueue Q){ if(Q.front==Q.rear) return true; else return false; } //入队 void EnQueue(LinkQueue &Q,ElemType x){ s=(LinkNode *)malloc(sizeof(LinkNode)); s->data=x; s->next=NULL; Q.rear->next=s; Q.rear=s; } //出队 void DeQueue(LinkNode &Q,Elemtype &x){ if(Q.front==Q.rear) return false; p=Q.front->next; x=P->data; Q.front->next=p->next; if(Q.rear==p) //若原队列中只有一个结点,删除后变空 Q.rear=Q.front; free(p); return true; }
双端队列
双端队列是指允许两端都可以进行入队和出队操作的
- 输出受限的双端队列:允许一端进行插入和删除,但在另一端只允许插入的双端队列
- 输入受限的双端队列:允许一端进行插入和删除,但在另一端只允许删除的双端队列
栈和队列的应用
- 中缀转后缀表达式
- 若为‘(’,入栈
- 若为‘)’,则依次把栈中的运算符加入后缀表达式,直到出现‘(’,并从栈中删除‘(’
- 若为‘+’,‘-’,‘*’,‘/’
- 栈空,入栈
- 栈顶元素为‘(’,入栈
- 高于栈顶元素优先级,入栈
- 否则,依次弹出栈顶运算符,直到一个优先级比他低的运算符或‘)’为止
*遍历完成,若栈非空,依次弹出所有元素
矩阵
压缩矩阵:指多个值相同的元素只分配一个存储空间,对零元素不分配存储空间。其目的是为了节省存储空间
特殊矩阵:指具有许多相同矩阵元素或零元素,并且这些相同矩阵元素或零元素的分布有一定规律性的矩阵。常见特殊矩阵有对称矩阵、上(下)三角矩阵、对角矩阵等
特殊矩阵的压缩存储方法:找出特殊矩阵中值相同的矩阵元素的分布规律,把那些呈现规律性分布的值的多个矩阵元素压缩存储到一个存储空间中
- 对称矩阵
元素下标的对应关系
k = i * ( i – 1 ) / 2 + j – 1 ; i >= j
k = j * ( j – 1 ) / 2 + i – 1 ; i < j - 三角矩阵
下三角矩阵元素下标的对应关系
k = i * ( i – 1 ) / 2 + j – 1 ; i >= j
k = n * ( n + 1 ) / 2 ; i < j
上三角矩阵元素下标的对应关系
k = ( i – 1 ) * ( 2n – i + 2 ) / 2 + ( j – i ) ; i <= j
k = n * ( n + 1 ) / 2 ; i > j - 三对角矩阵
元素下标的对应关系
k = 3 * ( i – 1 ) – 1 + j – i + 1 = 2i + j – 3
已知k求i、j
i = [ ( k + 1 ) / 3 + 1 ] ; j = k – 2i + 3
稀疏矩阵
矩阵元素个数远大于非零元素个数的矩阵
- 一般采用三元组(行标,列标,值)的形式存储
- 稀疏矩阵压缩存储后失去随机存取特性
四、树和二叉树
1.掌握树型结构的定义。
2.掌握二叉树的定义、性质及各种存贮结构。
3.掌握遍历二叉树、线索二叉树及其他基本操作。
4.掌握树、森林与二叉树的相互转换;理解树的遍历;掌握哈夫曼树及其应用。
五、图
1.掌握图的定义和术语。
2.掌握图的存贮结构;理解图的基本操作。
3.掌握图的遍历算法;了解利用图的遍历解决图的应用问题。
4.理解图的有关应用:求最小生成树、求最短路径、拓扑排序及关键路径等算法的基本思想。
六、查找
1.掌握静态查找表。
2.掌握二叉排序树和平衡二叉树。
3.理解B-树;了解B+树。
4.掌握哈希表。
5.掌握各种查找方法的时间性能分析。
七、内部排序
掌握直接插入排序、希尔排序、冒泡排序、快速排序、简单选择排序、堆排序、归并排序;理解基数排序。
1.插入排序
插入排序是一种简单直观的排序方法,其基本思想是每次将一个待排序的记录按其关键字大小插入前面已排好序的子序列,直到全部记录插入完成,由插入排序的思想可以引申三个重要的排序算法:直接插入排序,折半插入排序和希尔排序。
1.1直接插入排序
根据上面的插入排序思想,不难得出一种最简单也最直观的直接插入排序算法,假设在排序过程中,待排序表L[1…n]在某次排序过程重的某一个时刻状态如下:
要将元素L(i) 插入已有序的子序列L[1…i-1],需要执行以下操作(为避免混淆,下面用L[]表示一个表,而用L()表示一个元素):
1. 查找出L(i)在L(1…i-1)中的插入位置k。
2. 将L[k…i-1]中的所有元素依次后移一个位置。
3. 将L(i)复制到L(k)。
为了实现对L[1…n]的排序,可以将L(2)~L(n)依次插入前面已经排好的子序列,初始L[1]可以视为是一个已排好序的子序列。上述操作执行n-1次就能得到一个有序的表,插入排序在现实上通常采用就地排序(空间复杂度O(1))因而在从后向前的比较过程中,需要反复把已排序元素逐步向后挪位,为新元素提供插入空间。
下面是直接插入排序的代码:
void InsertSort(ElemType A[],int n){
int i, j;
for(i=2;i<=n;i++){ //y依次将A[2]~A[n]插入前面已排序序列
if(A[i]<A[i-1]){ //若A[i] 关键码小于其前驱,将A[i] 插入有序表
A[0]=A[i]; //复制为哨兵,A[0]不存放元素
for(j=i-1;A[0]<A[j];--j)//从后往前查找待插入位置
A[j+1]=A[j]; //向后挪位
A[j+1]=A[0]; //复制到插入位置
}
}
}
直接插入排序算法的性能分析如下:
空间效率:仅使用了常数个辅助单元,因而空间复杂度为O(1)。
时间效率:在排序过程中,向有序子表中逐个地插入元素的操作进行n-1趟,每趟操作都分为比较关键字和移动元素,而比较次数和移动次数取决于待排序表的初始状态。
在最好情况下,表中元素已经有序,此时每插入一个元素,都只需比较一次而不用移动元素,因而时间复杂度为O(n)。
在最坏情况下,表中元素顺序刚好与顺序结果中的元素顺序相反(逆序),总的比较次数达到最大,为(i+1)。
平均情况下,考虑待排序表中元素是随机的,此时可以取上述最好与最坏情况的平均值作为平均情况下的时间复杂度,总的比较次数与总的移动次数约为。
因此,直接插入排序算法的时间复杂度为O().
稳定性:由于每次插入元素时总是从后向前先比较再移动,所以不会出现相同元素对位置发生变化的情况,即直接插入排序是一个稳定的排序算法。
适用性:直接插入排序算法适用于顺存储和链式存储的线性表。为链式存储时,可以从前往后查找指定元素的位置。
1.2折半插入排序
从直接插入排序算法中,不难看出每趟插入的过程中都进行了两项工作:
1. 从嵌满的有序子表中查找出待插入元素应该被插入的位置;
2. 给插入位置腾出空间,将待插入元素复制到表中插入位置。注意到在该算法中,总是边比较边移动元素。下面将比较和移动操作分离,即先折半查找出元素的待插入位置,然后统一地移动待插入位置之后的所有元素。当排序表为顺序表时,可以对直接插入排序算法做如下改进:由于是顺序存储的线性表,所以查找有序子表时可以用折半查找来实现。确定待插入位置后,就可统一地向后移动元素。算法代码如下:
void InsertSort(ElementType A[],int n){
int i,j,low,high,mid;
for(i=2;i<=n;i++){ //依次将A[2]~A[n]插入前面的已排序序列
A[0]=A[i]; //将A[i]暂存到A[0]
low=1;high=i-1; //设置折半查找(默认递增有序)
while(low <=high ){
mid = (low+high)/2; //取中间点
if(A[mid]>A[0]) high=mid-1; //查找左半子表
else low = mid+1; //差找右半子表
}
for(j=i-1;;j>=high+1;--j)
A[j+1]=A[j]; //统一后移元素,空出插入位置
A[high+1]=A[0]; //插入操作
}
}
从上述算法中,不难看出折半插入排序不仅减少了比较元素的次数,约为O(nlog2 n),该比较次数与待排序表的初始状态无关,仅取决于表中的元素个数n;而元素的移动次数并未改变,它依赖于待排序表的初始状态,因此,折半插入排序的时间复杂度仍为O(),但对于数据量不很大的排序表,折半插入排序往往能表现出很好的性能。折半插入排序是一种稳定的排序方法。
1.3希尔排序
从前面的分析可知,直接插入排序算法的时间复杂度为O(),但若待排序列为“正序”时,其时间复杂度可提高至O(n),由此可见它更适用于基本有序的排序表和数据量不大的排序表。希尔排序正是基于这两点分析对直接插入排序进行改进而得来的,又称缩小增量排序。
希尔排序的基本思想是:先将待排序表分割成若干形如若L[i,i+d,i+2d,…,i+kd]的“特殊”子表,即把相隔某个“增量”的记录组成一个子表,对各个子表分别进行直接插入排序,当整个表中的元素已呈“基本有序”时,再对全体记录进行一次直接插入排序。
希尔排序的过程如下:先取一个小于n 的步长d1,把表中的全部记录分成d1组,所有距离为d1的倍数的记录放在同一组,在各组内进行直接插入排序;然后取第二个步长d2<d1,重复上述过程,直到所取到=1,即所有记录已放在同一组中,在进行直接插入排序,由于此时已经具有加号的局部有序性,故可以很快得到最终结果,到目前为止,尚未求得一个最好的增量序列。示例:第一趟取增量=5,将该序列分成5个子序列,即图中第2行至第6行,分别对各子序列进行直接插入排序,结果如第7行所示;第二趟取增量=3,分别对3个子序列进行直接插入排序,结果如第11行所示;最后对整个序列进行一趟直接插入排序,整个排序过程如图所示:
希尔排序算法的代码如下:
void ShellSort(ElemType A[],int n){
//A[0]只是暂存单元,不是哨兵,当j<=0时插入位置已到
for(dk=n/2;dk>=1;dk=dk/2)
for(i=dk+1;i<=n;++i) //步长变化
if(A[i]<A[i-dk]){
A[0]=A[i]; //暂存在A[0]
for(j=i-dk;j>0&& A[0]<A[j];j-=dk)
A[j+dk]=A[j]; //记录后移,查找插入的位置
A[j+dk]=A[0]; //插入
} //if
}
希尔排序算法的性能分析如下:
空间效率:仅使用了常数个辅助单元,因而空间复杂度为O(1).
时间效率:由于希尔排序的时间复杂度依赖于增量序列的函数,这涉及数学上尚未解决的难题,所以其时间复杂度分析比较困难。当n在某个特定范围时,希尔排序的时间复杂度约为O()。在最坏情况下希尔排序的时间复杂度为O()。
稳定性:当相同关键字的记录被划分到不同的子表时,可能会改变它们之间的相对次序,因此希尔排序是一种不稳定的排序方法。
适用性:希尔排序算法仅适用于线性表为顺序存储的情况。
2.交换排序
所谓交换,是指根据序列中两个元素关键字的比较结果来对换这两个记录在序列中的位置。
2.1冒泡排序
冒泡排序的基本思想是:从后往前(或从前往后)两两比较相邻元素的值,若为逆序(即A[i-1]>A[i]),则交换它们,直到序列比较完。我们称它为第一趟冒泡,结果是将最小的元素交换到待排序的第一个位置(或将最大的元素交换到待排序列的最后一个位置),关键字最小的元素如气泡一般逐渐往上“漂浮”直至“水面”(或关键字最大的元素如石头一般下沉至水底)。下一趟冒泡时,前一趟确定的最小元素不再参与比较,每趟冒泡的结果是把序列中的最小元素(或最大元素)放到了序列的最终位置……这样最多做n-1趟冒泡就能把所有元素排好序。
如图所示冒泡排序的过程,第一趟冒泡时:27<,不交换;13<27,不交换;76>13,交换;97>13,交换;65>13,交换;38>13,交换;49>13,交换。通过第一趟冒泡后,最小元素已交换到第一个位置,也是它的最终位置。第二趟冒泡时对剩余子序列采用同样方法进行排序,依次类推,到第5趟结束后没有发生交换,说明表已有序,冒泡排序结束。
冒泡排序算法的代码如下:
void BubbleSort(ElemType A[],int n){
for(i=0;i<n-1;i++){
flag = false; //表示本趟冒泡是否发生交换的标志
for(j=n-1;j>i;j--) //一趟冒泡过程
if(A[j-1]>A[j]){ //若为逆序
swap(A[j-1],A[j]); //交换
flag=true;
}
if(flag==flase)
return; //本趟遍历后没有发生交换,说明表已经有序
}
}
冒泡排序的性能分析如下:
空间效率:仅使用了常数个辅助单元,因而空间复杂度为O(1);
时间效率:当初始序列有序时,显然第一趟冒泡后flag 依然为false(本趟冒泡没有元素交换),从而直接跳出循环,比较次数为n-1,移动次数为0,从而最好情况下的时间复杂度为O(n);当初始序列为逆序时,需要进行n-1趟排序,第i趟排序要进行n-i次关键字的比较,而且每次比较后都必须移动元素3次交换元素位置。这种情况下,
比较次数=,移动次数=
从而,最坏情况下的时间复杂度为O() ,其平均时间复杂度也为O()。
稳定性:由于i>j且A[i]=A[j]时,不会发生交换,因此冒泡排序是一种稳定的排序方法。
注意:冒泡排序中产生的有序子序列一定是全局有序的(不同于直接插入排序),也就是说,有序子序列中所有元素的关键字一定小于或大于无序子序列中所有元素的关键字,这样每趟排序都会将一个元素放置到其最终的位置上。
2.2快速排序
快速排序的基本思想是基于分治的::在待排序表L[1…n]中任取一个元素pivot作为枢轴(或基准,通常取首元素),通过一趟排序将待排序表划分为独立的两部分L[1..k-1]和L[k+1…n],使得L[1…K-1]中的所有元素小于pivot ,L[k+1…n]中的所有元素大于等于pivot,则pivot放在了其最终L(K)上,这个过程称为一趟快速排序(或一次划分)。然后分别递归地随两个子表重复上述过程,直至每部分内只有一个元素或空为止,即所有元素放在了其最终位置上。
一趟快速排序的过程是一个交替搜索和交换的过程,下面通过实例来介绍,附设两个指针i和j ,初值分别为low和high,取第一个元素49为枢轴复制到变量pivot。
指针j从high往前搜索找到第一个小于枢轴的元素27,将27交换到i所指位置。
对算法的最好理解方式是搜总模拟一遍这些算法。
假设划分算法已知,记为Partition(),返回的是上述的k,注意到L(K)已经在最终的位置,因此可以先对表进行划分,而后对两个表调用同样的排序操作。因此可以递归地条用快速排序算法进行排序,具体的程序结构如下:
void QuickSort(ElemType A[],int low ,int high){
if(low<high){ //递归跳出的条件
int pivotpos=Partition(A,low,high); //划分
QuickSort(A,low ,pivotpos-1); //依次对连个子表进行递归排序
QuickSort(A,pivotpos+1,high);
}
}
从上面的代码不难看出快速排序算法的关键在于划分操作,同时快速排序算法的性能也主要取决于划分操作的好坏。从快速排序算法提出至今,已有许多不同的划分操作版本,但考研所考察的快速排序的划分操作基本以严蔚敏的教材《数据结构》为主,假设每次总以当前表中第一个元素作为枢轴来对表进行划分,则将表中比枢轴大的元素向右移动,将比枢轴小的元素向左移动,使得一趟Partition()操作后,表中的元素被枢轴值一分为二。代码如下:
int Partition(ElemType A[],int low ,int high){ //一趟划分
ElemType pivot = A[low]; //将当前表中第一个元素设置为枢轴,对表进行划分
while(low<high){ //循环跳出条件
while(low<high && A[high]>=pivot) --high;
A[low]=A[high]; //将比枢轴小的元素移动到左端
while(low<high && A[low]<=pivot) ++low;
A[high]=A[low]; //将比枢轴大的元素移动到右端
}
A[low]=pivot; //枢轴元素存放到最终位置
return low; //返回存放枢轴的最终位置
}
快速排序算法的性能分析如下:
空间效率:由于快速排序是递归的,需要借助一个递归工作栈来保存每层递归调用的必要信息,其容量应与递归调用的最大深度一致。最好情况下为O();最坏情况下,因为要进行n-1次递归调用,所以栈的深度为O(n);平均情况下,栈的深度为O(log)。
时间效率:快速排序的运行时间于划分是否对称有关,快速排序的最坏情况发生在两个区域分别包含n-1 个元素和0个元素时,这种最大限度的不对称性若发生在每层递归上,即对应于初始排序表基本有序或记恨逆序时,就得到最坏情况下的时间复杂度为O()。
有很多方法可以提高算法的效率:一种方法是尽量选取一个可以将数据中分的枢轴元素,若从序列的头尾及中间选取三个元素,再取这三个元素的中间值作为最终的枢轴元素;或者随机地从当前表中选取枢轴元素,这样做可是得最坏情况在实际排序中几乎不会发生。
在最理想的状态下,即Partition()可能做到最平衡的划分,得到的两个子问题的大小都不可能大于n/2,在这种情况下的,快速排序的运行速度将大大提升,此时,时间复杂度为O(nlog)。好在快速排序平均情况下运行时间与其最佳情况下的运行时间很接近,而不是接近最坏情况下的运行时间。快速排序是所有的内部排序算法中平均性能最优的排序算法。
稳定性: 在划分算法中,若右端区间有两个关键字相同,且均下雨基准的记录,则在交换到左端区间后,它们的相对位置就会发生变化,即快速排序是一种不稳地的排序方法。例如表L={3,2,2}经过一趟排序后L={2,2,3},最终排序序列也是L={2,2,3},显然,2与2的相对次序发生了变化。
注意:在快速排序算法中,并不产生有序子序列,但没每趟排序后会将枢轴(基准)元素方法其最终的位置上。
3.选择排序
选择排序的基本思想是:每一趟(如第i趟)在后面n-i+1(i=1,2,…,n-1)个待排序元素中选取关键字最小的元素,作为有序子序列的第i个元素,直到第n-1趟做完,待排序元素只剩下1个,就不用再选了。
3.1简单选择排序
根据上面选择排序的思想,可以很直观地得出简单选择排序算法的思想:假设排序表为L[1…n],第i趟排序即从L[i…n]中选择关键字最小的元素与L(i)交换,每一趟排序可以确定一个元素的最终位置,这样经过n-1趟排序就可以使得真个排序表有序。
简单选择排序算法的代码如下:
void SelectSort(ElemType A[],int n){
for(i=0,i<n-1;i++){ //一共进行n-1趟
min = i; // 记录最小元素位置
for(j=i+1;j<n;j++) //在A[i..n-1]中选取最小的元素
if(A[j]<A[min]) min=j; //更新最小元素位置
if(min!=i) swap(A[i],A[min]); //封装的swap()函数共移动元素3次
}
}
简单选择排序算法的性能分析如下:
空间效率:仅使用常数个辅助单元,故空间效率为O(1);
时间效率:从上述伪代码中不难看出,在简单选择排序的过程中,元素移动的操作次数很少,不会超过3(n-1)次,最好的情况下是移动0次,此时对应的表已经有序;但元素比较的次数与序列的初始状态无关,始终是n(n-1)/2次,因此时间复杂度始终是O()。
稳定性:在第i趟找到最小元素后,和第i个元素交换,可能会导致第i个元素与其含有相同关键字元素的相对位置发生改变,例如,表L={2,2,1},经过一趟排序后L={1,2,2},最终排序序列也是L={1,2,2},显示,2与2的相对次序已发生变化。因此,简单选择排序是一种不稳定的排序方法。
3.2堆排序
堆的定义如下,n个关键字序列L[1…n]成为堆,当且仅当该序列满足:
1. L(i)>=L(2i)且L(i)>(2i+1)或
2. L(i)<=L(2i)且L(i)<=L(2i+1) (1<=i<=)
可以将该一维数组视为一颗完全二叉树,满足条件1的成为大根堆(大顶堆),大根堆的最大元素存放在根节点,且其任一非根节点的值小于等于其双亲结点值。满足条件2的推成为小根堆(小顶堆),小根堆的定义刚好相反,根节点是最小元素。该图所示就是一个大根堆:
堆排序的思路很简单:首先将存放在L[1…n]中的n个元素建成初始堆,由于堆本身的特点(以大根堆为例),堆顶元素就是最大值。输出堆顶元素后,通常将堆低元素送入堆顶。此时根节点已不满足大顶堆的性质,堆被破坏,将堆顶元素向下调整使其继续保持大顶堆的性质,再输出堆顶元素。如此重复,直到堆中仅剩一个元素为止。可见堆排序需要解决两个问题:
1,如何将无序序列构造成初始堆?
2.输出堆顶元素后,如何将剩余元素调整成新的堆?
堆排序的关键是构造初始堆。n个节点的完全二叉树,最后一个节点是第个节点的孩子。对第个节点为根的子树筛选(对于大根堆,若根节点的关键字小于左右孩子中关键字较大者,则交换),使该子树成为堆。之后向前依次对各个节点(-1~1)为根的子树进行筛选,看该节点值是否大于其左右子节点的值,若不大于,则将左右子结点中的较大值与之交换,交换后可能会破坏下一级的堆,于是继续采用上述方法构造下一级的堆,直到以该结点为根的子树构成堆为止。反复利用上述调整对的方法建堆,直到根结点。
如图所示,初始时调整L(4)子树,09<32,交换,交换,交换后满足堆的定义;向前继续调整L(3)子树,78<左右孩子的较大者87,交换,交换后满足堆的定义;向前调整L(2)子树,17<左右孩子的较大者45,交换后满足堆的定义;向前调整至根结点L(1),53<左右孩子的较大者87,交换,交换后破坏了L(3)子树的推,采用上述方法对L(3)进行调整,53<左右孩子的较大者78,交换,至此该完全二叉树满足堆的定义。
输出堆顶元素后,将堆的最后一个元素与堆顶元素交换,此时堆的性质被破坏,需要向下进行筛选。将09和左右孩子的较大者78交换,交换后破坏了L(3)子树的堆,继续堆L(3)子树向下筛选,将09和左右孩子大的较大者65交换,交换后得到了新堆,调整过程如图所示。
下面是建立大根堆的算法:
void BuildMaxHeap(Elemtype A[],int len){
for(int i=len/2;i>0;i--) //从i=[n/2]~1,反复调整堆
HeadAdjust(A,i,ken);
}
void HeadAdjust(ElemType A[],int k,int len){
//函数HeadAjust 将元素k为根的子树进行调整
A[0] = A[k]; //A[0] 暂存子树的根结点
for(i=2*k;i<=len;i*=2){ //沿key较大的子结点向下筛选
if(i<len&& A[i]<A[]i+1)
i++; //取key 较大的子结点的下标
if(A[0]>=A[i]) break; //筛选结束
else{
A[k]=A[i]; //将A[i] 调整到双亲结点上
k=i; // 修改k值,以便继续向下筛选
}
}
A[k]=A[0]; //被筛选结点的值放入最终位置
}
调整的时间与树高有关,为O(h)。在建含n个元素的堆时,关键字的比较总次数不超过4n,时间复杂度为O(n),这说明在线性时间内将一个无序数组建成一个堆。
下面是堆排序算法:
void HeapSort(ElemType A[] ,int len){
BuildMaxHeap(A,len); //初始建模
for(i=len;i>1;i--){ //n-1趟的交换和建堆过程
Swap(A[i],A[1]); //输出堆顶元素(和堆低元素交换)
HeadAdjust(A,1,i-1); //调整,把剩余的i-1个元素调整成堆
}
}
同时,堆也支持插入操作。对堆进行插入操作时,先将新结点放在堆的末端,再对这个新结点向上执行调整操作。大根堆的插入操作如图所示:
堆排序适合关键字较多的情况,例如,在1亿个数中选出前100个最大值?首先使用一个大小为100的数组,读入前100个数,建立小顶堆,而后依次读入余下的数,若小于堆顶则舍弃,否则用该数取代堆顶并重新调整堆,待数据读取完毕,堆中100个数即为所求。
堆排序算法的性能分析如下:
空间效率:仅使用了常数个辅助单元,所以空间复杂度为O(1);
时间效率:建堆时间为O(n),之后有n-1次向下调整操作,每次调整的时间复杂度为O(h),故在最好,最坏和平均情况下,堆排序的时间复杂度为O(n)。
稳定性:进行筛选时,有可能把后面相同关键字的元素调整到前面,所以堆排序算法是一种不稳定的排序算法。例如,表L={1,2,2},构造初始堆时可能将2交换到堆顶,此时L={
2,1,2},最总排序序列为L={1,2,2},显然,2与2的相对次序已发生变化。
4.归并排序和基数排序
4.1归并排序
4.2基数排序
2.学会各种内部排序方法的比较(时间复杂度、空间复杂度、稳定性)。
今天的文章数据结构知识点总结归纳_数据结构期末考试题分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/74503.html