第一章:绪论
1.1数据结构的基本概念
1.数据:数据是信息的载体,是描述客观事物属性的数、字符以及所有能输入到计算机中并被程序识别和处理的符号的集合。
2.数据元素:数据元素是数据的基本单位,通常作为一个整体进行考虑和处理。一个数据元素可由若干数据项组成,数据项是构成数据元素的不可分割的最小单位。例如,学生记录就是一个数据元素,它由学号、姓名、性别等数据项组成。
3.数据对象:数据对象是具有相同性值的数据元素的集合,是数据的一个子集。
4.数据类型:数据类型是一个值的集合和定义再此集合上的一组操作的总称。
1)原子类型。其值不可再分的数据类型。如bool 和int 类型。 2)结构类型。其值可以再分解为若干成分(分量)的数据类型。 3)抽象数据类型。抽象数据组织及与之相关的操作。
5.数据结构:数据结构是相互之间存在一种或多种特定关系的数据元素的集合。
1.2数据结构的三要素
1.数据的逻辑结构: 逻辑结构是指数据元素之间的逻辑关系,即从逻辑关系上描述数据。 逻辑结构包括:
- 集合结构:结构中的数据元素之间除“同属一个集合”外,别无其它关系。
- 线性结构:结构中的数据元素之间只存在一对一的关系,除了第一个元素,所有元素都有唯一前驱;除了最后一个元素,所有元素都有唯一后继。
- 树形结构:结构中数据元素之间存在一对多的关系。
- 图状结构:数据元素之间是多对多的关系。
2.数据的存储结构(物理结构): 存储结构是指数据结构在计算机中的表示(又称映像),也称物理结构。 存储结构包括:
- 顺序存储:把逻辑上相邻的元素存储在物理位置也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
- 链式存储:逻辑上相邻的元素在物理位置上可以不相邻,借助指示元素存储地址的指针来表示元素之间的逻辑关系。
- 索引存储:在存储元素信息的同时,还建立附加的索引表,索引表中的每项称为索引项,索引项的一般形式是(关键字,地址)
- 散列存储:根据元素的关键字直接计算出该元素的存储地址,又称哈希(Hash)存储。
3.数据的运算:施加在数据上的运算包括运算的定义何实现。运算的定义是针对逻辑结构的,指出运算的功能;运算的实现是针对存储结构的,指出运算的具体操作步骤。
1.3算法的基本概念
程序=数据结构+算法 算法(algorithm)是对特定问题求解步骤的一种描述,它是指令的有限序列,其中的每条指令表示一个或多个操作。 算法的特性: 1.有穷性:一个算法必须总在执行有穷步之后结束,且每一步都可在有穷时间内完成。 2.确定性:算法中每条指令必须有确定的含义,对于相同的输入只能得到相同的输出。 3.可行性:算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现。 4.输入:一个算法有零个或多个输入,这些输入取自于某个特定的对象的集合。 5.输出:一个算法有一个多个输出,这些输出是与输入有着某种特定关系的量。 好的算法达到的目标:
- 正确性:算法应能够正确的求接问题。
- 可读性:算法应具有良好的可读性,以帮助人们理解。
- 健壮性:输入非法数据时,算法能适当地做出反应或进行处理,而不会产生莫名奇妙地输出结果。
- 效率与低存储量需求:效率是指算法执行的时间,存储量需求是指算法执行过程中所需要的最大存储空间,这两者都与问题的规模有关。
1.4算法的时间复杂度
一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数f(n),算法的时间量度记作T(n)=O(n),它表示随问题规模n的增大而增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称时间复杂度。
1.5算法的空间复杂度
算法的空间复杂度S(n)定义为该算法所耗费的存储空间,它是问题规模n的函数。记为S(n)=O(g(n))。
第二章:线性表
2.1线性表的定义
线性表是具有相同数据类型的n(n>0)个数据元素的有限序列,其中n为表长,当n=0时线性表是一个空表。
2.2顺序表的定义
2.2.1静态分配:
//顺序表的实现--静态分配
typedef struct{
int data[MaxSize];//用静态的"数组"存放数据元素
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义(静态分配方式)
void InitList(SqList &L){
for(int i=0;i<MaxSize;i++){
L.data[i]=0; //将所有数据元素设置为默认初始值
}
L.length=0;
}
int main(){
SqList L;//声明一个顺序表
InitList(L);//初始化一个顺序表
for(int i=0;i<MaxSize;i++){
printf("data[%d]=%d\n",i,L.data[i]);
}
return 0;
}
2.2.2动态分配
//顺序表的实现——动态分配
typedef struct{
int *data;//指示动态分配数组的指针
int MaxSize; //顺序表的最大容量
int length; //顺序表的当前长度
}SeqList;
//初始化
void InitList(SeqList &L){
//用malloc 函数申请一片连续的存储空间
L.data =(int*)malloc(InitSize*sizeof(int)) ;
L.length=0;
L.MaxSize=InitSize;
}
//增加动态数组的长度
void IncreaseSize(SeqList &L,int len){
int *p=L.data;
L.data=(int*)malloc((L.MaxSize+len)*sizeof(int));
for(int i=0;i<L.length;i++){
L.data[i]=p[i]; //将数据复制到新区域
}
L.MaxSize=L.MaxSize+len; //顺序表最大长度增加len
free(p); //释放原来的内存空间
}
int main(void){
SeqList L; //声明一个顺序表
InitList(L);//初始化顺序表
IncreaseSize(L,5);
return 0;
}
顺序表的特点:
- 随机访问 ,可以在O(1)时间内找到第i个元素。
- 存储密度高,每个节点只存储数据元素
- 拓展容量不方便
- 插入、删除操作不方便,需要移动大量元素
2.2顺序表的基本操作
1.插入操作 :平均时间复杂度O(n)
bool ListInsert(SqList &L, int i, int e){
//判断i的范围是否有效
if(i<1||i>L.length+1)
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处放入e
L.length++; //长度加1
return true;
}
2.删除操作:平均时间复杂度O(n)
bool LisDelete(SqList &L, int i, int &e){ // e用引用型参数
//判断i的范围是否有效
if(i<1||i>L.length)
return false;
e = L.data[i-1] //将被删除的元素赋值给e
for(int j=L.length; j>i; j--){ //将第i个后的元素前移
L.data[j-1]=L.data[j];
}
L.length--; //长度减1
return true;
}
3.按位查找(获取L表中第i个位置的值):平均时间复杂度O(1)
typedef struct{
ElemType data[MaxSize]; //用静态的“数组”存放数据元素
int Length; //顺序表的当前长度
}SqList; //顺序表的类型定义
ElemType GetElem(SqList L, int i){
// ...判断i的值是否合法
return L.data[i-1]; //注意是i-1
}
4.按值查找:平均时间复杂度O(n)
typedef struct{
ElemTyp *data; //用静态的“数组”存放数据元素
int Length; //顺序表的当前长度
}SqList;
//在顺序表L中查找第一个元素值等于e的元素,并返回其位序
int LocateElem(SqList L, ElemType e){
for(int i=0; i<L.lengthl i++)
if(L.data[i] == e)
return i+1; //数组下标为i的元素值等于e,返回其位序i+1
return 0; //推出循环,说明查找失败
}
2.3线性表的链式表示
2.3.1 单链表的定义
定义: 线性表的链式存储又称单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素。
typedef struct LNode{//定义单链表结点类型
ElemType data; //数据域
struct LNode *next;//指针域
}LNode, *LinkList;
可以利用typedef关键字——数据类型重命名:type<数据类型><别名>
单链表的两种实现方式:
- 不带头结点的单链表
```bash
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
//初始化一个空的单链表
bool InitList(LinkList &L){ //注意用引用 &
L = NULL; //空表,暂时还没有任何结点;
return true;
}
void test(){
LinkList L; //声明一个指向单链表的指针: 头指针
//初始化一个空表
InitList(L);
//...
}
//判断单链表是否为空
bool Empty(LinkList L){
if (L == NULL)
return true;
else
return false;
}
头结点:代表链表上头指针指向的第一个结点,不带有任何数据。
- 带头结点的单链表
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
//初始化一个单链表(带头结点)
bool InitList(LinkList &L){
L = (LNode*) malloc(sizeof(LNode)); //头指针指向的结点——分配一个头结点(不存储数据)
if (L == NULL) //内存不足,分配失败
return false;
L -> next = NULL; //头结点之后暂时还没有结点
return true;
}
void test(){
LinkList L; //声明一个指向单链表的指针: 头指针
//初始化一个空表
InitList(L);
//...
}
//判断单链表是否为空(带头结点)
bool Empty(LinkList L){
if (L->next == NULL)
return true;
else
return false;
}
带头结点和不带头结点的比较: 不带头结点:写代码麻烦!对第一个数据节点和后续数据节点的处理需要用不同的代码逻辑,对空表和非空表的处理也需要用不同的代码逻辑; 头指针指向的结点用于存放实际数据; 带头结点:头指针指向的头结点不存放实际数据,头结点指向的下一个结点才存放实际数据;
2.3.2单链表上基本操作的实现
1.按位序插入(带头结点): ==ListInsert(&L, i, e): ==在表L中的第i个位置上插入指定元素e = 找到第i-1个结点(前驱结点),将新结点插入其后;其中头结点可以看作第0个结点,故i=1时也适用。
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
//在第i个位置插入元素e(带头结点)
bool ListInsert(LinkList &L, int i, ElemType e){
//判断i的合法性, i是位序号(从1开始)
if(i<1)
return False;
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点
p = L; //L指向头结点,头结点是第0个结点(不存数据)
//循环找到第i-1个结点
while(p!=NULL && j<i-1){ //如果i>lengh, p最后会等于NULL
p = p->next; //p指向下一个结点
j++;
}
if (p==NULL) //i值不合法
return false;
//在第i-1个结点后插入新结点
LNode *s = (LNode *)malloc(sizeof(LNode)); //申请一个结点
s->data = e;
s->next = p->next;
p->next = s; //将结点s连到p后,后两步千万不能颠倒qwq
return true;
}
平均时间复杂度:O(n)
2.按位序插入(不带头结点) ==ListInsert(&L, i, e): ==在表L中的第i个位置上插入指定元素e = 找到第i-1个结点(前驱结点),将新结点插入其后; 因为不带头结点,所以不存在“第0个”结点,因此!i=1 时,需要特殊处理——插入(删除)第1个元素时,需要更改头指针L;
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
bool ListInsert(LinkList &L, int i, ElemType e){
if(i<1)
return false;
//插入到第1个位置时的操作有所不同!
if(i==1){
LNode *s = (LNode *)malloc(size of(LNode));
s->data =e;
s->next =L;
L=s; //头指针指向新结点
return true;
}
//i>1的情况与带头结点一样!唯一区别是j的初始值为1
LNode *p; //指针p指向当前扫描到的结点
int j=1; //当前p指向的是第几个结点
p = L; //L指向头结点,头结点是第0个结点(不存数据)
//循环找到第i-1个结点
while(p!=NULL && j<i-1){ //如果i>lengh, p最后会等于NULL
p = p->next; //p指向下一个结点
j++;
}
if (p==NULL) //i值不合法
return false;
//在第i-1个结点后插入新结点
LNode *s = (LNode *)malloc(sizeof(LNode)); //申请一个结点
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
3.指定结点的后插操作:
InsertNextNode(LNode *p, ElemType e): 给定一个结点p,在其之后插入元素e; 根据单链表的链接指针只能往后查找,故给定一个结点p,那么p之后的结点我们都可知,但是p结点之前的结点无法得知;
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
bool InsertNextNode(LNode *p, ElemType e){
if(p==NULL){
return false;
}
LNode *s = (LNode *)malloc(sizeof(LNode));
//某些情况下分配失败,比如内存不足
if(s==NULL)
return false;
s->data = e; //用结点s保存数据元素e
s->next = p->next;
p->next = s; //将结点s连到p之后
return true;
} //平均时间复杂度 = O(1)
//有了后插操作,那么在第i个位置上插入指定元素e的代码可以改成:
bool ListInsert(LinkList &L, int i, ElemType e){
if(i<1)
return False;
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点
p = L; //L指向头结点,头结点是第0个结点(不存数据)
//循环找到第i-1个结点
while(p!=NULL && j<i-1){ //如果i>lengh, p最后4鸟会等于NULL
p = p->next; //p指向下一个结点
j++;
}
return InsertNextNode(p, e)
}
4.指定结点的前插操作 思想:设待插入结点是s,将s插入到p的前面。我们仍然可以将s插入到*p的后面。然后将p->data与s->data交换,这样既能满足了逻辑关系,又能是的时间复杂度为O(1).(真是妙的不达鸟)
//前插操作:在p结点之前插入元素e
bool InsertPriorNode(LNode *p, ElenType e){
if(p==NULL)
return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
if(s==NULL) //内存分配失败
return false;
//重点来了!
s->next = p->next;
p->next = s; //新结点s连到p之后
s->data = p->data; //将p中元素复制到s
p->data = e; //p中元素覆盖为e
return true;
} //时间复杂度为O(1)
王道书代码:
bool InsertPriorNode(LNode *p, LNode *s){
if(p==NULL || S==NULL)
return false;
s->next = p->next;
p->next = s; ///s连接到p
ELemType temp = p->data; //交换数据域部分
p->data = s->data;
s->data = temp;
return true;
}
5.按位序删除节点(带头结点) ListDelete(&L, i, &e): 删除操作,删除表L中第i个位置的元素,并用e返回删除元素的值;头结点视为“第0个”结点;
思路:找到第i-1个结点,将其指针指向第i+1个结点,并释放第i个结点;
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
bool ListDelete(LinkList &L, int i, ElenType &e){
if(i<1) return false;
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点
p = L; //L指向头结点,头结点是第0个结点(不存数据)
//循环找到第i-1个结点
while(p!=NULL && j<i-1){ //如果i>lengh, p最后会等于NULL
p = p->next; //p指向下一个结点
j++;
}
if(p==NULL)
return false;
if(p->next == NULL) //第i-1个结点之后已无其他结点
return false;
LNode *q = p->next; //令q指向被删除的结点
e = q->data; //用e返回被删除元素的值
p->next = q->next; //将*q结点从链中“断开”
free(q) //释放结点的存储空间
return true;
}
时间复杂度分析: 最坏,平均时间复杂度:O(n)
最好时间复杂度:删除第一个结点 O(1)
6.指定结点的删除
bool DeleteNode(LNode *p){
if(p==NULL)
return false;
LNode *q = p->next; //令q指向*p的后继结点
p->data = p->next->data; //让p和后继结点交换数据域
p->next = q->next; //将*q结点从链中“断开”
free(q);
return true;
} //时间复杂度 = O(1)
2.3.3单链表的查找
按位查找 ==GetElem(L, i): ==按位查找操作,获取表L中第i个位置的元素的值;
LNode * GetElem(LinkList L, int i){
if(i<0) return NULL;
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点
p = L; //L指向头结点,头结点是第0个结点(不存数据)
while(p!=NULL && j<i){ //循环找到第i个结点
p = p->next;
j++;
}
return p; //返回p指针指向的值
}
平均时间复杂度O(n)
按值查找 LocateElem(L, e):按值查找操作,在表L中查找具有给定关键字值的元素;
LNode * LocateElem(LinkList L, ElemType e){
LNode *P = L->next; //p指向第一个结点
//从第一个结点开始查找数据域为e的结点
while(p!=NULL && p->data != e){
p = p->next;
}
return p; //找到后返回该结点指针,否则返回NULL
}
2.3.4求单链表的长度
== Length(LinkList L)==:计算单链表中数据结点(不含头结点)的个数,需要从第一个结点看是顺序依次访问表中的每个结点。算法的时间复杂度为O(n)。
int Length(LinkList L){
int len=0; //统计表长
LNode *p = L;
while(p->next != NULL){
p = p->next;
len++;
}
return len;
}
2.3.5单链表的创建操作
1.头插法建立单链表(平均时间复杂度O(n)) 思路:每次都将生成的结点插入到链表的表头。
LinkList List_HeadInsert(LinkList &L){ //逆向建立单链表
LNode *s;
int x;
L = (LinkList)malloc(sizeof(LNode)); //建立头结点
L->next = NULL; //初始为空链表,这步不能少!
scanf("%d", &x); //输入要插入的结点的值
while(x!=9999){ //输入9999表结束
s = (LNode *)malloc(sizeof(LNode)); //创建新结点
s->data = x;
r->next = L->next;
L->next = s; //将新结点插入表中,L为头指针
scanf("%d", &x);
}
return L;
}
2.尾插法建立单链表(时间复杂度O(n)) 思路:每次将新节点插入到当前链表的表尾,所以必须增加一个尾指针r,使其始终指向当前链表的尾结点。好处:生成的链表中结点的次序和输入数据的顺序会一致。
LinkList List_TailInsert(LinkList &L){ //正向建立单链表
int x; //设ElemType为整型int
L = (LinkList)malloc(sizeof(LNode)); //建立头结点(初始化空表)
LNode *s, *r = L; //r为表尾指针
scanf("%d", &x); //输入要插入的结点的值
while(x!=9999){ //输入9999表结束
s = (LNode *)malloc(sizeof(LNode));
s->data = x;
r->next = s;
r = s //r指针指向新的表尾结点
scanf("%d", &x);
}
r->next = NULL; //尾结点指针置空
return L;
}
链表的逆置: 算法思想:逆置链表初始为空,原表中结点从原链表中依次“删除”,再逐个插入逆置链表的表头(即“头插”到逆置链表中),使它成为逆置链表的“新”的第一个结点,如此循环,直至原链表为空;
LNode *Inverse(LNode *L)
{
LNode *p, *q;
p = L->next; //p指针指向第一个结点
L->next = NULL; //头结点指向NULL
while (p != NULL){
q = p;
p = p->next;
q->next = L->next;
L->next = q;
}
return L;
2.3.6双链表
双链表中节点类型的描述:`
typedef struct DNode{
ElemType data;
struct DNode *prior, *next;
}DNode, *DLinklist;
双链表的初始化(带头结点)
typedef struct DNode{
ElemType data;
struct DNode *prior, *next;
}DNode, *DLinklist;
bool InitDLinkList(Dlinklist &L){
L = (DNode *)malloc(sizeof(DNode));
if(L==NULL)
return false;
L->prior = NULL;
L->next = NULL;
return true;
}
void testDLinkList(){
DLinklist L;
InitDLinkList(L);
}
bool Empty(DLinklist L){
if(L->next == NULL)
return true;
else
return false;
}
双链表的插入操作 后插操作 InsertNextDNode(p, s): 在p结点后插入s结点
bool InsertNextDNode(DNode *p, DNode *s){
if(p==NULL || s==NULL)
return false;
s->next = p->next;
if (p->next != NULL)
p->next->prior = s;
s->prior = p;
p->next = s;
return true;
}
按位序插入操作: 思路:从头结点开始,找到某个位序的前驱结点,对该前驱结点执行后插操作; 前插操作: 思路:找到给定结点的前驱结点,再对该前驱结点执行后插操作; 双链表的删除操作 删除p节点的后继节点
bool DeletNextDNode(DNode *p){
if(p==NULL) return false;
DNode *q =p->next;
if(q==NULL) return false;
p->next = q->next;
if(q->next != NULL)
q->next->prior=p;
free(q);
return true;
}
bool DestoryList(DLinklist &L){
while(L->next != NULL){
DeletNextDNode(L);
free(L);
L=NULL;
}
}
双链表的遍历操作 前向遍历
while(p!=NULL){
p = p->prior;
}
后向遍历
while(p!=NULL){
p = p->next;
}
注意:双链表不可随机存取,按位查找和按值查找操作都只能用遍历的方式实现,时间复杂度为O(n)
2.3.7循环链表
1.循环单链表 最后一个结点的指针不是NULL,而是指向头结点
typedef struct LNode{
ElemType data;
struct LNode *next;
}DNode, *Linklist;
/初始化一个循环单链表
bool InitList(LinkList &L){
L = (LNode *)malloc(sizeof(LNode));
if(L==NULL)
return false;
L->next = L;
return true;
}
bool Empty(LinkList L){
if(L->next == L)
return true;
else
return false;
}
bool isTail(LinkList L, LNode *p){
if(p->next == L)
return true;
else
return false;
}
单链表和循环单链表的比较: **单链表:**从一个结点出发只能找到该结点后续的各个结点;对链表的操作大多都在头部或者尾部;设立头指针,从头结点找到尾部的时间复杂度=O(n),即对表尾进行操作需要O(n)的时间复杂度; **循环单链表:**从一个结点出发,可以找到其他任何一个结点;设立尾指针,从尾部找到头部的时间复杂度为O(1),即对表头和表尾进行操作都只需要O(1)的时间复杂度;
2.循环双链表 表头结点的prior指向表尾结点,表尾结点的next指向头结点
typedef struct DNode{
ElemType data;
struct DNode *prior, *next;
}DNode, *DLinklist;
bool InitDLinkList(DLinklist &L){
L = (DNode *) malloc(sizeof(DNode));
if(L==NULL)
return false;
L->prior = L;
L->next = L;
}
void testDLinkList(){
DLinklist L;
InitDLinkList(L);
}
bool Empty(DLinklist L){
if(L->next == L)
return true;
else
return false;
}
bool isTail(DLinklist L, DNode *p){
if(p->next == L)
return true;
else
return false;
}
双链表的插入(循环双链表):
bool InsertNextDNode(DNode *p, DNode *s){
s->next = p->next;
p->next->prior = s;
s->prior = p;
p->next = s;
双链表的删除
p->next = q->next;
q->next->prior = p;
free(q);
2.3.8静态链表
1. 定义:
2.静态链表用代码表示:
#define MaxSize 10
struct Node{
ElemType data;
int next;
};
void testSLinkList(){
struct Node a[MaxSize];
}
也可以这样:
#define MaxSize 10
typedef struct{
ELemType data;
int next;
}SLinkList[MaxSize];
void testSLinkList(){
SLinkList a;
}
也等同于:
#define MaxSize 10
struct Node{
ElemType data;
int next;
};
typedef struct Node SLinkList[MaxSize];
注意:SLinkList a 强调a是静态链表;struct Node a 强调a是一个Node型数组;
3.静态链表基本操作的实现
-
初始化静态链表:把a[0]的next设为-1 -
查找某个位序(不是数组下标,位序是各个结点在逻辑上的顺序)的结点:从头结点出发挨个往后遍历结点,时间复杂度O=(n) -
在位序为i上插入结点:① 找到一个空的结点,存入数据元素;② 从头结点出发找到位序为i-1的结点;③修改新结点的next;④ 修改i-1号结点的next; -
删除某个结点:① 从头结点出发找到前驱结点;② 修改前驱节点的游标;③ 被删除节点next设为-2;
2.3.9 顺序表和链表的比较
1.逻辑结构
2.存储结构
-
顺序表:顺序存储
- 优点:支持随机存取,存储密度高
- 缺点:大片连续空间分配不方便,改变容量不方便
-
链表:链式存储
- 优点:离散的小空间分配方便,改变容量方便
- 缺点:不可随机存取,存储密度低
3. 基本操作 - 创建
-
顺序表:需要预分配大片连续空间。若分配空间过小,则之后不方便拓展容量;若分配空间过大,则浪费内存资源; -
静态分配:静态数组,容量不可改变 -
动态分配:动态数组,容量可以改变,但是需要移动大量元素,时间代价高(malloc(),free()) -
链表:只需要分配一个头结点或者只声明一个头指针
4. 基本操作 - 销毁
typedef struct{
ElemType *data;
int MaxSize;
int length;
}SeqList;
L.data = (ELemType *)malloc(sizeof(ElemType) *InitSize)
free(L.data);
5.基本操作-增/删
6.基本操作-查
第三章:栈和队列
3.1栈(stack)
3.1.1栈的基本概念
1.栈的定义
- 栈是特殊的线性表:只允许在一端进行插入或删- 除操作, 其逻辑结构与普通线性表相同;
- 栈顶(Top):允许进行插入和删除的一端 (最上面的为栈顶元素);
- 栈底(Bottom):固定的,不允许进行插入和删除的一端 (最下面的为栈底元素);
- 空栈:不含任何元素的空表;
- 特点:后进先出(后进栈的元素先出栈);
- 缺点:栈的大小不可变,解决方法——共享栈;
2.栈的基本操作
"创建&销毁"
- InitStack(&S) 初始化栈:构造一个空栈S,分配内存空间;
- DestroyStack(&S) 销毁栈:销毁并释放栈S所占用的内存空间;
"增&删"
- Push(&S, x) 进栈:若栈S未满,则将x加入使其成为新栈顶;
- Pop(&S, &x) 出栈:若栈S非空,则弹出(删除)栈顶元素,并用x返回;
"查&其他" - GetTop(S, &x) 读取栈顶元素:若栈S非空,则用x-返回栈顶元素;(栈的使用场景大多只访问栈顶元素);
- StackEmpty(S) 判空: 断一个栈S是否为空,若S为空,则返回true,否则返回false;
3.1.2 栈的顺序存储
1.顺序栈的定义
#define MaxSize 10
typedef struct{
ElemType data[MaxSize];
int top;
}SqStack;
void testStack(){
SqStack S;
}
2.顺序栈的基本操作
#define MaxSize 10
typedef struct{
ElemType data[MaxSize];
int top;
}SqStack;
void InitStack(SqStack &S){
S.top = -1;
}
bool StackEmpty(SqStack S){
if(S.top == -1)
return true;
else
return false;
}
bool Push(SqStack &S, ElemType x){
if(S.top == MaxSize - 1)
return false;
S.top = S.top + 1;
S.data[S.top] = x;
return true;
}
bool Pop(SqStack &x, ElemType &x){
if(S.top == -1)
return false;
x = S.data[S.top];
S.top = S.top - 1;
return true;
}
bool GetTop(SqStack S, ElemType &x){
if(S.top == -1)
return false;
x = S.data[S.top];
return true;
}
void testStack(){
SqStack S;
InitStack(S);
}
**注意:**也可以初始化时定义 S.top = 0 :top指针指向下一个可以插入元素的位置(栈顶元素的后一个位置);
- 进栈操作 :栈不满时,栈顶指针先加1,再送值到栈顶元素。
S.data[S.top++] = x; - 出栈操作:栈非空时,先取栈顶元素值,再将栈顶指针减1。`x = S.data[–S.top];
- 栈空条件:S.top==-1
- 栈满条件:S.top==MaxSize-1
- 栈长:S.top+1
3.共享栈 **定义:**利用栈底位置相对不变的特性,可以让两个顺序栈共享一个一维数组空间,将两个栈的栈底 分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸。
#define MaxSize 10
typedef struct{
ElemType data[MaxSize];
int top0;
int top1;
}ShStack;
void InitSqStack(ShStack &S){
S.top0 = -1;
S.top1 = MaxSize;
}
栈满条件:top1-top0=1
3.1.3栈的链式存储
1.定义:采用链式存储的栈称为链栈。 2.优点:链栈的优点是便于多个栈共享存储空间和提高其效率,且不存在栈满上溢的情况。 3.特点:
- 进栈和出栈都只能在栈顶一端进行(链头作为栈顶)
- 链表的头部作为栈顶,意味着:
1. 在实现数据"入栈"操作时,需要将数据从链表的头部插入; 2. 在实现数据"出栈"操作时,需要删除链表头部的首元节点; 因此,链栈实际上就是一个只能采用头插法插入或删除数据的链表; 栈的链式存储结构可描述为:
typedef struct Linknode{
ElemType data;
struct Linknode *next;
}*LiStack;
- 栈的基本操作:
- 初始化
- 进栈
- 出栈
- 获取栈顶元素
- 判空、判满
带头结点的链栈基本操作:
#include<stdio.h>
struct Linknode{
int data;
Linknode *next;
}Linknode,*LiStack;
typedef Linknode *Node;
typedef Node List;
void InitStack(LiStack &L){
L = new Linknode;
L->next = NULL;
}
bool isEmpty(LiStack &L){
if(L->next == NULL){
return true;
}
else
return false;
}
void pushStack(LiStack &L, int x){
Linknode s;
s = new Linknode;
s->data = x;
s->next = L->next;
L->next = s;
}
bool popStack(LiStack &L, int &x){
Linknode s;
if(L->next == NULL)
return false;
s = L->next;
x = s->data;
L->next = L->next->next;
delete(s);
return true;
}
不带头结点的链栈基本操作:
#include<stdio.h>
struct Linknode{
int data;
Linknode *next;
}Linknode,*LiStack;
typedef Linknode *Node;
typedef Node List;
void initStack(LiStack &L){
L=NULL;
}
bool isEmpty(LiStack &L){
if(L == NULL)
return true;
else
teturn false;
}
void pushStack(LiStack &L, int x){
Linknode s;
s = new Linknode;
s->next = L;
L = s;
}
bool popStack(LiStack &L, int &x){
Linknode s;
if(L = NULL)
return false;
s = L;
x = s->data;
L = L->next;
delete(s);
return true;
}
3.2队列(Queue)
3.2.1队列的基本概念
1.定义:队列(Queue)简称队,是一种操作受限的线性表,只允许在表的一端进行插入,而在表的另一端进行删除。 2.特点
- 队列是操作受限的线性表,只允许在一端进行插入 (入队),另一端进行删除 (出队)
- 操作特性:先进先出 FIFO
- 队头:允许删除的一端
- 队尾:允许插入的一端
- 空队列:不含任何元素的空表
3.队列的基本操作 "创建&销毁" InitQueue(&Q): 初始化队列,构造一个空列表QDestroyQueue(&Q): 销毁队列,并释放队列Q所占用的内存空间 "增&删"EnQueue(&Q, x): 入队,若队列Q未满,将x加入,使之成为新的队尾DeQueue(&Q, &x): 出队,若队列Q非空,删除队头元素,并用x返回 "查&其他"GetHead(Q,&x): 读队头元素,若队列Q非空,则将队头元素赋值给xQueueEmpty(Q): 判队列空,若队列Q为空,则返回
3.2.2队列的顺序存储结构
- 队头指针:指向队头元素
- 队尾指针:指向队尾元素的下一个位置
- 队列存储的基本操作
# define MaxSize 10;
typedef struct{
ElemType data[MaxSize];
int front, rear;
}SqQueue;
void InitQueue(SqQueue &Q){
Q.rear = Q.front = 0;
}
void test{
SqQueue Q;
InitQueue(Q);
}
bool QueueEmpty(SqQueue 0){
if(Q.rear == Q.front)
return true;
else
return false;
}
- 循环队列
定义:将循环队列臆造为一个环状的空间,即把存储队列元素的表从逻辑上视为一个环,称为循环队列。 基本操作:
a%b == a除以b的余数
初始: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
- 循环队列——入队:只能从队尾插入(判满使用方案一)
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;
}
bool GetHead(SqQueue &Q, ElemType &x){
if(Q.rear == Q.front)
return false;
x = Q.data[Q.front];
return true;
}
方案二: 不牺牲存储空间,设置size 定义一个变量 size 用于记录队列此时记录了几个数据元素,初始化 size = 0 ,进队成功 size++ ,出队成功size-- ,根据size的值判断队满与队空
队满条件:size == MaxSize
队空条件:size == 0
# define MaxSize 10;
typedef struct{
ElemType data[MaxSize];
int front, rear;
int size;
}SqQueue;
void InitQueue(SqQueue &Q){
Q.rear = Q.front = 0;
size = 0;
}
方案三: 不牺牲存储空间,设置tag 定义一个变量 tag ,tag = 0 --最近进行的是删除操作;tag = 1 --最近进行的是插入操作;
每次删除操作成功时,都令tag = 0 ;只有删除操作,才可能导致队空; 每次插入操作成功时,都令tag = 1 ;只有插入操作,才可能导致队满; 队满条件:Q.front == Q.rear && tag == 1
队空条件:Q.front == Q.rear && tag == 0
# define MaxSize 10;
typedef struct{
ElemType data[MaxSize];
int front, rear;
int tag;
}SqQueue;
3.2.3队列的链式存储结构
1.定义:队列的链式表示称为链队列,它实际上是一个同时带有队头指针和队尾指针的单链表。 队列的链式存储类型可描述为:
typedef struct LinkNode{
ElemType data;
struct LinkNode *next;
}
typedef struct{
LinkNode *front, *rear;
}LinkQueue;
2.链式队列的基本操作——带头结点
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){
LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode));
s->data = x;
s->next = NULL;
Q.rear->next = s;
Q.rear = s;
}
bool DeQueue(LinkQueue &Q, ElemType &x){
if(Q.front == Q.rear)
return false;
LinkNode *p = Q.front->next;
x = p->data;
Q.front->next = p->next;
if(Q.rear == p)
Q.rear = Q.front;
free(p);
return true;
}
链式存储:一般不会队满,除非内存不足
void EnQueue(LinkQueue &Q, ElemType x){
LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode));
s->data = x;
s->next = NULL;
if(Q.front = NULL){
Q.front = s;
Q.rear = s;
}else{
Q.rear->next = s;
Q.rear = s;
}
}
3.2.4双端队列
1.定义:双端队列是指允许两端都可以进行入队和出队操作的队列
- 双端队列允许从两端插入、两端删除的线性表;
- 如果只使用其中一端的插入、删除操作,则等同于栈;
- 输入受限的双端队列:允许一端插入,两端删除的线性表;
- 输出受限的双端队列:允许两端插入,一端删除的线性表;
3.3栈的应用
3.3.1栈在括号匹配中的应用
用栈实现括号匹配
代码:
#define MaxSize 10
typedef struct{
char data[MaxSize];
int top;
} SqStack;
InitStack(SqStack &S)
bool StackEmpty(SqStack &S)
bool Push(SqStack &S, char x)
bool Pop(SqStack &S, char &x)
bool bracketCheck(char str[], int length){
SqStack S;
InitStack(S);
for(int i=0; i<length; i++){
if(str[i] == '(' || str[i] == '[' || str[i] == '{'){
Push(S, str[i]);
}else{
if(StackEmpty(S))
return false;
char topElem;
Pop(S, topElem);
if(str[i] == ')' && topElem != '(' )
return false;
if(str[i] == ']' && topElem != '[' )
return false;
if(str[i] == '}' && topElem != '{' )
return false;
}
}
StackEmpty(S);
}
3.3.2栈在表达式求值中的应用
1. 中缀表达式 (需要界限符) 运算符在两个操作数中间:
① a + b
② a + b - c
③ a + b - c*d
④ ((15 ÷ (7-(1+1)))×3)-(2+(1+1))
⑤ A + B × (C - D) - E ÷ F
2. 后缀表达式 (逆波兰表达式) 运算符在两个操作数后面:
① a b +
② ab+ c - / a bc- +
③ ab+ cd* -
④ 15 7 1 1 + - ÷ 3 × 2 1 1 + + -
⑤ A B C D - × + E F ÷ - (机算结果)
A B C D - × E F ÷ - + (不选择)
中缀表达式转后缀表达式-手算 步骤1: 确定中缀表达式中各个运算符的运算顺序
步骤2: 选择下一个运算符,按照[左操作数 右操作数 运算符]的方式组合成一个新的操作数
步骤3: 如果还有运算符没被处理,继续步骤2
“左优先”原则: 只要左边的运算符能先计算,就优先算左边的 (保证运算顺序唯一);
中缀:A + B - C * D / E + F
① ④ ② ③ ⑤
后缀:A B + C D * E / - F +
重点:中缀表达式转后缀表达式-机算 初始化一个栈,用于保存暂时还不能确定运算顺序的运算符。从左到右处理各个元素,直到末尾。可能遇到三种情况:
遇到操作数: 直接加入后缀表达式。 遇到界限符: 遇到 ‘(’ 直接入栈; 遇到 ‘)’ 则依次弹出栈内运算符并加入后缀表达式,直到弹出 ‘(’ 为止。注意: '(' 不加入后缀表达式。 遇到运算符: 依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到 ‘(’ 或栈空则停止。之后再把当前运算符入栈。 按上述方法处理完所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达式。
后缀表达式的计算—手算: 从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行对应的运算,合体为一个操作数;
注意: 两个操作数的左右顺序
重点:后缀表达式的计算—机算 用栈实现后缀表达式的计算(栈用来存放当前暂时不能确定运算次序的操作数)
步骤1: 从左往后扫描下一个元素,直到处理完所有元素;
步骤2: 若扫描到操作数,则压入栈,并回到步骤1;否则执行步骤3;
步骤3: 若扫描到运算符,则弹出两个栈顶元素,执行相应的运算,运算结果压回栈顶,回到步骤1;
注意: 先出栈的是“右操作数”
3.前缀表达式 (波兰表达式) 运算符在两个操作数前面:
① + a b
② - +ab c
③ - +ab *cd
中缀表达式转前缀表达式—手算 步骤1: 确定中缀表达式中各个运算符的运算顺序
步骤2: 选择下一个运算符,按照[运算符 左操作数 右操作数]的方式组合成一个新的操作数
步骤3: 如果还有运算符没被处理,就继续执行步骤2
“右优先”原则: 只要右边的运算符能先计算,就优先算右边的;
中缀:A + B * (C - D) - E / F
⑤ ③ ② ④ ①
前缀:+ A - * B - C D / E F
前缀表达式的计算—机算 用栈实现前缀表达式的计算
步骤1: 从右往左扫描下一个元素,直到处理完所有元素;
步骤2: 若扫描到操作数则压入栈,并回到步骤1,否则执行步骤3
步骤3: 若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到步骤1;
注意: 先出栈的是“左操作数”
4.中缀表达式的计算(用栈实现) 两个算法的结合: 中缀转后缀 + 后缀表达式的求值
初始化两个栈,操作数栈 和运算符栈
若扫描到操作数,压人操作数栈
若扫描到运算符或界限符,则按照“中缀转后缀”相同的逻辑压入运算符栈 (期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈项元素并执行相应运算,运算结果再压回操作数栈)
3.3.3栈在递归中的应用
函数调用的特点:最后被调用的函数最先执行结束(LIFO)
函数调用时,需要用一个栈存储:
递归调用时,函数调用栈称为 “递归工作栈”:
- 每进入一层递归,就将递归调用所需信息压入栈顶;
- 每退出一层递归,就从栈顶弹出相应信息;
**缺点:**太多层递归可能回导致栈溢出;
适合用“递归”算法解决:可以把原始问题转换为属性相同,但规模较小的问题;
3.4特殊矩阵的压缩存储
3.4.1数组的存储结构
1.一维数组
Elemtype a[10];
各数组元素大小相同,物理上连续存放;
起始地址:LOC
数组下标:默认从0开始!
数组元素 a[i] 的存放地址 = LOC + i × sizeof(ElemType)
2.二维数组
Elemtype b[2][4];
行优先/列优先存储优点:实现随机存储
起始地址:LOC
M行N列的二维数组 b[M][N] 中,b[i][j] 的存储地址:
行优先存储: LOC + (i×N + j) × sizeof(ElemType) 列优先存储:LOC + (j×M + i) × sizeof(ElemType)
3.4.2普通矩阵的存储
二维数组存储:
- 描述矩阵元素时,行、列号通常从1开始;
- 描述数组时,通常下标从 0 开始;
3.4.3特殊矩阵的存储
特殊矩阵——压缩存储空间(只存有用的数据)
-
对称矩阵(方阵) -
三角矩阵(方阵) -
三对角矩阵(方阵) -
稀疏矩阵
第四章:串
4.1串的定义和实现
4.1.1串的定义
- 串: 零个或多个字符组成的有限序列,如
S = 'iPhone 11 Pro Max?'; - 串名:S是串名;
- 串的长度:串中字符的个数n;
- 空串:n=0时的串;
- 子串:串中任意多个连续的字符组成的子序列称为该串的子串;
- 主串:包含子串的串;
- 字符在主串中的位置:某个字符在串中的序号(从1开始);
- 子串在主串中的位置:子串的第一个字符在主串中的位置;
- 空串 V.S 空格串:
M = ‘’ 是空串; N = ’ ’ 是空格串; - 串 V.S 线性表:
串是特殊的线性表,数据元素之间呈线性关系(逻辑结构相似); 串的数据对象限定为字符集:中文字符、英文字符、数字字符、标点字符… 串的基本操作,如增删改除通常以子串为操作对象
4.1.2串的基本操作
假设有串 T = '' , S = 'iPhone 11 Pro Max?' , W = 'Pro'
StrAssign(&T, chars) : 赋值操作,把串T赋值为chars;StrCopy(&T, S) : 复制操作,把串S复制得到串TStrEmpty(S) : 判空操作,若S为空串,则返回TRUE,否则返回False;StrLength(S) : 求串长,返回串S的元素个数;ClearString(&S) : 清空操作,将S清为空串;DestroyString(&S) : 销毁串,将串S销毁——回收存储空间;Concat(&T, S1, S2) : 串联联接,用T返回由S1和S2联接而成的新串———可能会导致存储空间的扩展; 例:
Concat(&T, S, W)
T = ‘iPhone 11 Pro Max?Pro’
SubString(&Sub, S, pos, len) : 求子串,用Sub返回串S的第pos个字符起长度为len的子串;
SubString(&T, S, 4, 6)
T = ‘one 11’
Index(S, T) : 定位操作,若主串S中存在与串T值相同的子串,则返回它再主串S中第一次出现的位置,否则函数值为0;StrCompare(S, T): 串的比较操作,参照英文词典排序方式;若S > T,返回值>0; S = T,返回值=0 (需要两个串完全相同) ; S < T,返回值<0;
4.1.3串的存储结构
1定长顺序存储表示
#define MAXLEN 255
typedef struct{
char ch[MAXLEN];
int length;
}SString;
-
串长的两种表示法: -
方案一:用一个额外的变量length来存放串的长度(保留ch[0]); -
方案二:用ch[0]充当length; 优点:字符的位序和数组下标相同; -
方案三:没有length变量,以字符’\0’表示结尾(对应ASCII码的0); 缺点:需要从头到尾遍历; -
**方案四——最终使用方案:**ch[0]废弃不用,声明int型变量length来存放串的长度(方案一与方案二的结合) -
基本操作实现(基于方案四)
#define MAXLEN 255
typedef struct{
char ch[MAXLEN];
int length;
}SString;
bool SubString(SString &Sub, SString S, int pos, int len){
if (pos+len-1 > S.length)
return false;
for (int i=pos; i<pos+len; i++)
Sub.cn[i-pos+1] = S.ch[i];
Sub.length = len;
return true;
}
int StrCompare(SString S, SString T){
for (int i; i<S.length && i<T.length; i++){
if(S.ch[i] != T.ch[i])
return S.ch[i] - T.ch[i];
}
return S.length - T.length;
}
int Index(SString S, SString T){
int i=1;
n = StrLength(S);
m = StrLength(T);
SString sub;
while(i<=n-m+1){
SubString(Sub,S,i,m);
if(StrCompare(Sub,T)!=0)
++i;
else
return i;
}
return 0;
}
2.堆分配存储表示
typedef struct{
char *ch;
int length;
}HString;
HString S;
S.ch = (char *) malloc(MAXLINE * sizeof(char));
S.length;
3.串的链式存储
typedef struct StringNode{
char ch;
struct StringNode *next;
}StringNode, * String;
问题:存储密度低,每个字符1B,每个指针4B; 解决方案:每一个链表的结点存储多个字符——每个结点称为块——块链结构
typedef struct StringNode{
char ch[4];
struct StringNode *next;
}StringNode, * String;
结合链表思考优缺点
- 存储分配角度:链式存储的字符串无需占用连续空间,存储空间分配更灵活;
- 操作角度:若要在字符串中插入或删除某些字符,则顺序存储方式需要移动大量字符,而链式存储不用;
- 若要按位序查找字符,则顺序存储支持随机访问,而链式存储只支持顺序访问;
4.2串的模式匹配
模式匹配:子串的定位操作称为串的模式,它求的是子串(常称模式串)在主串中的位置。
4.2.1朴素模式匹配算法
int Index(SString S, SString T){
int i=1;
int j=1;
while(i<=S.length && j<=T.length){
if(S.ch[i] == T.ch[j]){
++i;
++j;
}
else{
i = i-j+2;
j=1;
}
}
if(j>T.length)
return i-T.length;
else
return 0;
}
时间复杂度分析:
- 主串长度为n,模式串长度为m
最多比较n-m+1 个子串 最坏时间复杂度 = O(nm) 每个子串都要对比m个字符(对比到最后一个字符才匹配不上),共要对比n-m+1个子串,复杂度 = O((n-m+1)m) = O(nm - m^2 + m) = O(nm) PS:大多数时候,n>>m 最好时间复杂度 = O(n) 每个子串的第一个字符就匹配失败,共要对比n-m+1个子串,复杂度 = O(n-m+1) = O(n)
4.2.2改进的模式匹配算法——KMP算法
- 不匹配的字符之前,一定是和模式串一致的;
- 根据模式串T,求出next数组(只与模式串有关,与主串无关),利用next数组进行匹配,当匹配失败时,主串的指针 i 不再回溯!
next数组是根据子串求出来的,当前面的字符串已知时如果有重复的,从当前的字符匹配即可。
- 求next数组
- 作用:当模式串的第j个字符失配时,从模式串的第next[j]继续往后匹配;
- 对于任何模式串,当第1个字符不匹配时,只能匹配下一个子串,因此,next[1] = 0——表示模式串应右移一位,主串当前指针后移一位,再和模式串的第一字符进行比较;
- 对于任何模式串,当第2个字符不匹配时,应尝试匹配模式串的第一个字符,因此,next[2] = 0;
例:对于串 T = 'abaabc'
- 利用
next数组 进行模式匹配
int Index_KMP(SString S, SString T, int next[]){
int i=1;
int j=1;
while(i<S.length && j<=T.length){
if(j==0 || S.ch[i]==T.ch[j]){
++j;
++i;
}
else
j=next[j]
}
if(j>T.length)
return i-T.length;
}
- 时间复杂度分析
- 求next数组时间复杂度 = O(m)
- 模式匹配过程最坏时间复杂度 = O(n)
- KMP算法的最坏时间复杂度 = O(m+n)
第五章:树
5.1树的基本概念
5.1.1树的定义
树是n个结点的有限集。
5.1.2基本术语
- 结点之间的关系描述
- 祖先、子孙、双亲、兄弟…结点
- 路径、路径长度
- 结点、树的属性描述
1. 结点的层次(深度)——从上往下 2. 结点的高度——从下往上 3. 树的高度——总共多少层 4. 结点的度——有几个孩子 5. 树的度——各结点的度的最大值 - 有序树、无序树
- 森林
5.1.3树的性质
- 树中的结点数等于所有结点的度数之和加1。
- 度为m的树第i层上至多有m^i-1个结点
- 度为m的数、m叉数的区别
5.2二叉树的概念
5.2.1 二叉树的定义与特性
- 二叉树有左右之分,次序不能颠倒
5.2.2几种特殊的二叉树
- 满二叉树
- 完全二叉树
- 二叉排序树
- 平衡二叉树
5.2.3二叉树的存储结构
- 顺序存储
二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来;
#define MaxSize 100
struct TreeNode{
ElemType value;
bool isEmpty;
}
main(){
TreeNode t[MaxSize];
for (int i=0; i<MaxSize; i++){
t[i].isEmpty = true;
}
}
- 链式存储
struct ElemType{
int value;
};
typedef struct BiTnode{
ElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
BiTree root = NULL;
root = (BiTree) malloc (sizeof(BiTNode));
root -> data = {1};
root -> lchild = NULL;
root -> rchild = NULL;
BiTNode *p = (BiTree) malloc (sizeof(BiTNode));
p -> data = {2};
p -> lchild = NULL;
p -> rchild = NULL;
root -> lchild = p;
- 找到指定结点p的左/右孩子;
- 找到指定结点p的父节点;只能从根结点开始遍历,也可以使用三叉链表
typedef struct BiTnode{
ElemType data;
struct BiTNode *lchild, *rchild;
struct BiTNode *parent;
}BiTNode, *BiTree;
5.3二叉树的遍历和线索二叉树
5.3.1二叉树的遍历
- 先序遍历(根左右)
typedef struct BiTnode{
ElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
void PreOrder(BiTree T){
if(T!=NULL){
visit(T);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
- 中序遍历(左根右)
typedef struct BiTnode{
ElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
void InOrder(BiTree T){
if(T!=NULL){
InOrder(T->lchild);
visit(T);
InOrder(T->rchild);
}
}
- 后续遍历(左右根)
- 若二叉树为空,不用操作
- 若二叉树非空:
- 先序遍历左子树 - 先序遍历右子树 - 访问根节点
typedef struct BiTnode{
ElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
void PostOrder(BiTree T){
if(T!=NULL){
PostOrder(T->lchild);
PostOrder(T->rchild);
visit(T);
}
}
- 二叉树的层次遍历
算法思想:
- 初始化一个辅助队列
- 根节点入队
- 若队列非空,则队头结点出队,访问该结点,依次将其左、右孩子插入队尾(如果有的话)
- 重复以上操作直至队列为空
typedef struct BiTnode{
ElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
typedef struct LinkNode{
BiTNode * data;
typedef LinkNode *next;
}LinkNode;
typedef struct{
LinkNode *front, *rear;
}LinkQueue;
void LevelOrder(BiTree T){
LinkQueue Q;
InitQueue (Q);
BiTree p;
EnQueue(Q,T);
while(!isEmpty(Q)){
DeQueue(Q,p);
visit(p);
if(p->lchild != NULL)
EnQueue(Q,p->lchild);
if(p->rchild != NULL)
EnQueue(Q,p->rchild);
}
}
- 由遍历序列构造二叉树
- 先序序列 + 中序序列
- 后序序列 + 中序序列
- 层序序列 + 中序序列
key: 找到树的根节点,并根据中序序列划分左右子树,再找到左右子树根节点、
5.3.2线索二叉树
-
线索二叉树的概念与作用 -
线索二叉树的存储结构
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag;
}ThreadNode, *ThreadTree;
tag == 0: 指针指向孩子
tag == 1: 指针是“线索”
-
先序线索二叉树——线索指向先序前驱、先序后继 -
后序线索二叉树——线索指向后序前驱、后序后继
- 二叉树的线索话
typedef struct ThreadNode{
int data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag;
}ThreadNode, *ThreadTree;
TreadNode *pre=NULL;
void InThread(ThreadTree T){
if(T!=NULL){
InThread(T->lchild);
visit(T);
InThread(T->rchild);
}
}
void visit(ThreadNode *q){
if(q->lchid = NULL){
q->lchild = pre;
q->ltag = 1;
}
if(pre!=NULL && pre->rchild = NULL){
pre->rchild = q;
pre->rtag = 1;
}
pre = q;
}
void CreateInThread(ThreadTree T){
pre = NULL;
if(T!=NULL);{
InThread(T);
if(pre->rchild == NULL)
pre->rtag=1;
}
}
- 先序线索化
注意【转圈】问题,当ltag==0时,才能对左子树先序线索化
typedef struct ThreadNode{
int data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag;
}ThreadNode, *ThreadTree;
TreadNode *pre=NULL;
void PreThread(ThreadTree T){
if(T!=NULL){
visit(T);
if(T->ltag == 0)
PreThread(T->lchild);
PreThread(T->rchild);
}
}
void visit(ThreadNode *q){
if(q->lchid = NULL){
q->lchild = pre;
q->ltag = 1;
}
if(pre!=NULL && pre->rchild = NULL){
pre->rchild = q;
pre->rtag = 1;
}
pre = q;
}
void CreateInThread(ThreadTree T){
pre = NULL;
if(T!=NULL);{
PreThread(T);
if(pre->rchild == NULL)
pre->rtag=1;
}
}
typedef struct ThreadNode{
int data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag;
}ThreadNode, *ThreadTree;
TreadNode *pre=NULL;
void PostThread(ThreadTree T){
if(T!=NULL){
PostThread(T->lchild);
PostThread(T->rchild);
visit(T);
}
}
void visit(ThreadNode *q){
if(q->lchid = NULL){
q->lchild = pre;
q->ltag = 1;
}
if(pre!=NULL && pre->rchild = NULL){
pre->rchild = q;
pre->rtag = 1;
}
pre = q;
}
void CreateInThread(ThreadTree T){
pre = NULL;
if(T!=NULL);{
PostThread(T);
if(pre->rchild == NULL)
pre->rtag=1;
}
}
- 线索二叉树中找前驱、后继
- 中序线索二叉树找中序后继:在中序线索二叉树中找到指定节点 *p 的中序后继 next
若 p->rtag == 1, 则 next = p->rchild;
若 p->rtag == 0, 则 p 必有右孩子, 则 next = p的右子树中最左下结点;
ThreadNode *Firstnode(ThreadNode *p){
while(p->ltag == 0)
p=p->lchild;
return p;
}
ThreadNode *Nextnode(ThreadNode *p){
if(p->rtag==0)
return Firstnode(p->rchild);
else
return p->rchild;
}
void Inorder(ThreadNode *T){
for(ThreadNode *p = Firstnode(T); p!=NULL; p = Nextnode(p))
visit(p);
}
- 先序线索二叉树找先序后继:在先序线索二叉树中找到指定节点 *p 的先序后继 next
若 p->rtag == 1, 则 next = p->rchild; 若 p->rtag == 0 , 则 p 必有右孩子(左孩子不知道)
case1: 若p有左孩子 ——— 根 左 右 / 根 (根 左 右) 右
case2: 若p没有左孩子 ——— 根 右 / 根 (*根 *左 右)
- 先序线索二叉树找先序前驱:在先序线索二叉树中找到指定节点 *p 的先序前驱pre
若 p->ltag == 1, 则 next = p->lchild;
若 p->ltag == 0, 则 p 必有左孩子,但是先序遍历中,左右子树的结点只可能是根的后继,不可能是前驱,所以不能从左右孩子里寻找p的先序前驱,(除非从头开始遍历/三叉链表
case1: 如果能够找到p的父节点,且p是左孩子 —— p的父节点就是p的前驱;
case2: 如果能够找到p的父节点,且p是右孩子,且其左兄弟为空 —— p的父节点就是p的前驱;
case3: 如果能够找到p的父节点,且p是右孩子,且其左兄弟非空 —— p的前驱为左兄弟子树中最后一个被先序遍历到的结点(根节点出发,先往右,右没有往左,找到最下一层的结点);
case4: p没有父节点,即p为根节点,则p没有先序前驱
- 后序线索二叉树找后序前驱:在后序线索二叉树中找到指定节点 *p 的后序前驱pre
若 p->ltag == 1, 则 next = p->lchild;
若 p->ltag == 0, 则 p 必有左孩子(不知道有没有右孩子)
case1: 若p有右孩子 ——— 左 右 根 / 左 (左 右 根) 根
case2: 若p没有右孩子 ——— 左 根 (左子树按后序遍历,最后一个结点,p的左孩子)
- 后序线索二叉树找后序后继:在后序线索二叉树中找到指定节点 *p 的后序后继next
若 p->rtag == 1, 则 next = p->rchild;
若 p->rtag == 0, 则 p 必有右孩子, 左孩子不知道, 但是在后序遍历中,左右子树中的结点只有可能是根的前驱,而不可能是根的后继,所以找不到后继,(除非从头开始遍历/三叉链表
case1: 如果能找到p的父节点,且p是右孩子 —— p的父节点即为其后继
case2: 如果能找到p的父节点,且p是左孩子,其右兄弟为空 —— p的父节点即为其后继
case3: 如果能找到p的父节点,且p是左孩子,其右兄弟非空 —— p的后继为其右兄弟子树中第一个被后序遍历的结点;
case4: p没有父节点,即p为根节点,则p没有后序后继;
5.4树、森林
5.4.1树的存储结构
- 双亲表示法(顺序存储):每个结点中保存指向双亲的指针
#define MAX_TREE_SIZE 100
typedef struct{
ElemType data;
int parent;
}PTNode;
typedef struct{
PTNode nodes[MAX_TREE_SIZE];
int n;
}PTree;
增:新增数据元素,无需按逻辑上的次序存储;(需要更改结点数n) 删(叶子结点):① 将伪指针域设置为-1;②用后面的数据填补;(需要更改结点数n) 查询:①优点-查指定结点的双亲很方便;②缺点-查指定结点的孩子只能从头遍历,空数据导致遍历更慢;
- 孩子表示法(顺序+链式)
struct CTNode{
int child;
struct CTNode *next;
};
typedef struct{
ElemType data;
struct CTNode *firstChild;
}CTBox;
typedef struct{
CTBox nodes[MAX_TREE_SIZE];
int n, r;
}CTree;
- 孩子兄弟表示法(链式)
typedef struct CSNode{
ElemType data;
struct CSNode *firstchild, *nextsibling;
}CSNode. *CSTree;
5.4.2树、森林与二叉树的转换
本质:森林中各个树的根结点之间视为兄弟关系
5.4.3树、森林的遍历
- 树的遍历
- 先根遍历:若树非空,先访问根结点,再依次对每棵子树进行先根遍历;(与对应二叉树的先序遍历序列相同)
void PreOrder(TreeNode *R){
if(R!=NULL){
visit(R);
while(R还有下一个子树T)
PreOrder(T);
}
}
- 后根遍历:若树非空,先依次对每棵子树进行后根遍历,最后再返回根节点;(与对应二叉树的中序遍历序列相同)
void PostOrder(TreeNode *R){
if(R!=NULL){
while(R还有下一个子树T)
PostOrder(T);
visit(R);
}
}
若树非空,则根结点入队; 若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队; 重复以上操作直至队尾为空;
- 森林的遍历
- 先序遍历:等同于依次对各个树进行先根遍历;也可以先转换成与之对应的二叉树,对二叉树进行先序遍历;
- 中序遍历:等同于依次对各个树进行后根遍历;也可以先转换成与之对应的二叉树,对二叉树进行中序遍历;
5.5树与二叉树的应用
5.5.1二叉排序树(BST)
- 二叉排序树的定义
左子树结点值<跟结点值<右子树结点值 - 查找操作
typedef struct BSTNode{
int key;
struct BSTNode *lchild, *rchild;
}BSTNode, *BSTree;
BSTNode *BST_Search(BSTree T, int key){
while(T!=NULL && key!=T->key){
if(key<T->key)
T = T->lchild;
else
T = T->rchild;
}
return T;
}
BSTNode *BSTSearch(BSTree T, int key){
if(T == NULL)
return NULL;
if(Kry == T->key)
return T;
else if(key < T->key)
return BSTSearch(T->lchild, key);
else
return BSTSearch(T->rchild, key);
}
- 插入操作
int BST_Insert(BSTree &T, int k){
if(T==NULL){
T = (BSTree)malloc(sizeof(BSTNode));
T->key = k;
T->lchild = T->rchild = NULL;
return 1;
}
else if(K == T->key)
return 0;
else if(k < T->key)
return BST_Insert(T->lchild,k);
else
return BST_Insert(T->rchild,k);
}
- 二叉排序树的构造
void Crear_BST(BSTree &T, int str[], int n){
T = NULL;
int i=0;
while(i<n){
BST_Insert(T,str[i]);
i++;
}
}
- 删除操作
- 查找效率分析
查找长度:查找运算中,需要对比关键字的次数,反映了查找操作时间复杂度; 查找成功的平均查找长度ASL 查找失败的平均查找长度ASL
5.5.2平衡二叉树(AVL)
- 平衡二叉树的定义
在插入和删除二叉树的结点时,要保证任意结点的左右子树的高度差的绝对值不超过1,将这样的树称为平衡二叉树。
typedef struct AVLNode{
int key;
int balance;
struct AVLNode *lchild; *rchild;
}AVLNode, *AVLTree;
- 平衡二叉树的插入
- 插入新节点后如何调整“不平衡”问题
调整最小不平衡子树
LL: 在A结点的左孩子的左子树中插入导致不平衡 调整: A的左孩子结点右上旋 RR: 在A结点的右孩子的右子树中插入导致不平衡 调整: A的右孩子结点左上旋 LR: 在A结点的左孩子的右子树中插入导致不平衡 调整: A的左孩子的右孩子,先左上旋再右上旋 RL: 在A结点的右孩子的左子树中插入导致不平衡 调整: A的右孩子的左孩子,先右上旋再左上旋
- 平衡二叉树的查找与效率分析
若树高为h,则最坏情况下,查找一个关键字最多需要对比h次,即查找操作的时间复杂度不可能超过O(h);
5.5.3哈夫曼树
- 带权路径长度
- 哈夫曼树的定义
- 哈夫曼树的构造(重点)
- 哈杜曼编码(重点)
第六章 图
第七章 查找
第八章 排序
8.1排序的基本概念
- 排序:重新排列表中的元素,使表中元素满足按关键字有序的过程(关键字可以相同)
- 排序算法的评价指标:时间复杂度、空间复杂度;
- 排序算法的稳定性:关键字相同的元素在排序之后相对位置不变,称为稳定的;(选择题考查)
Q: 稳定的排序算法一定比不稳定的好? A: 不一定,要看实际需求; - 排序算法的分类:
内部排序: 数据都在内存——关注如何使时间、空间复杂度更低; 外部排序: 数据太多,无法全部放入内存——关注如何使时间、空间复杂度更低,如何使读/写磁盘次数更少;
8.2 插入排序
8.2.1直接插入排序
- 算法思想: 每次将一个待排序的记录按其关键字大小,插入(依次对比、移动)到前面已经排好序的子序列中,直到全部记录插入完成
- 代码实现:
void InsertSort(int A[], int n){
int i,j,temp;
for(i=1; i<n; i++)
if(A[i]<A[i-1]){
temp = A[i];
for(j=i-1; j>=0 && A[j]>temp; --j)
A[j-1] = A[j];
A[j+1] = temp;
}
}
void InsertSort(int A[], int n){
int i,j;
for(i=1; i<n; i++)
if(A[i]<A[i-1]){
A[0] = A[i];
for(j=i-1; A[0] < A[j]; --j)
A[j+1] = A[j];
A[j+1] = A[0];
}
}
- 算法效率分析
空间复杂度:O(1) 时间复杂度:主要来自于对比关键字、移动关键字,若有n个元素,则需要n-1躺处理 最好情况: 原本为有序,共n-1趟处理,每一趟都只需要对比1次关键字,不需要移动元素,共对比n-1次 —— O(n) 最差情况: 原本为逆序 —— O(n2) 平均情况: O(n2) 算法稳定性:稳定
- 对链表进行插入排序
移动元素的次数变少了,因为只需要修改指针,不需要依次右移; 但是关键字对比的次数依然是O(n2)数量级,因此整体看来时间复杂度仍然是O(n2)
8.2.2折半插入排序
-
思路: 先用折半查找找到应该插入的位置,再移动元素; -
为了保证稳定性,当查找到和插入元素关键字一样的元素时,应该继续在这个元素的右半部分继续查找以确认位置; 即当 A[mid] == A[0] 时,应继续在mid所指位置右边寻找插入位置 -
当low>high时,折半查找停止,应将[low,i-1]or[high+1,i-1]内的元素全部右移,并将A[0]复制到low所指的位置; -
代码实现
void InsertSort(int A[], int n){
int i,j,low,high,mid;
for(i=2;i<=n;i++){
A[0] = A[i];
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(n2)
8.2.3希尔排序
-
思路: 先追求表中元素的部分有序,再逐渐逼近全局有序; -
更适用于基本有序的排序表和数据量不大的排序表,仅适用于线性表为顺序存储的情况 -
代码实现:
void ShellSort(ElemType A[], int n){
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];
for(j=i-dk; j>0&&A[0]<A[j];j-=dk)
A[j+dk]=A[j];
A[j+dk]=A[0;]
}
}
- 算法效率分析
- 空间效率:空间复杂度=O(1)
- 时间效率: 最坏情况下时间复杂度=O(n2)
- 稳定性:希尔排序是一种不稳定的排序方法
8.3 交换排序
**基于“交换”的排序:**根据序列中两个元素关键字的比较结果来对换这两个记录再序列中的位置;
8.3.1冒泡排序
-
第一趟排序使关键字值最小的一个元素“冒”到最前面(其最终位置)—— 每趟冒泡的结果是把序列中最小元素放到序列的最终位置,这样最多做n-1趟冒泡就能把所有元素排好序; -
为保证稳定性,关键字相同的元素不交换; -
代码实现
void swap(int &a, int &b){
int temp = a;
a = b;
b = temp;
}
void BubbleSort(int A[], int n){
for(int i=0; i<n-1; i++){
bool flag = false;
for(int j=n-1; j>i; j--)
if(A[j-1]>A[j]){
swap(A[j-1],A[j]);
flag=true;
}
if(flag==false)
return;
}
}
- 算法效率分析
空间复杂度:O(1) 时间复杂度 最好情况 (有序) :只需要一趟排序,比较次数=n-1,交换次数=0,最好时间复杂度=O(n) 最坏情况 (逆序) :比较次数 = (n-1)+(n-2)+…+1 = n(n-1)/2 = 交换次数,最坏时间复杂度 = O(n2),平均时间复杂度 = O(n2) 冒泡排序可以用于链表、顺序表
8.3.2快速排序
- 每一趟排序都可使一个中间元素确定其最终位置
- 用一个元素(不一定是第一个)把待排序序列“划分”为两个部分,左边更小,右边更大,该元素的最终位置已确认
- 算法实现(重点)
int Partition(int A[], int low, int high){
int 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;
}
void QuickSort(int A[], int low, int high){
if(low<high)
int pivotpos = Partition(A, low, high);
QuickSort(A, low, pivotpos - 1);
QuickSort(A, pivotpos + 1, high);
}
- 算法效率分析
每一层的QuickSort只需要处理剩余的待排序元素,时间复杂度不超过O(n);
把n个元素组织成二叉树,二叉树的层数就是递归调用的层数,n个结点的二叉树最小高度 = ?log?n? + 1, 最大高度 = n
时间复杂度 = O(n×递归层数) (递归层数最大为n)
最好 = O(nlog?n) : 每次选的枢轴元素都能将序列划分成均匀的两部分; 最坏 = O(n2) :序列本就有序或逆序,此时时间、空间复杂度最高; 平均时间复杂度 = O(nlog?n) (接近最好而不是最坏) 空间复杂度 = O(递归层数)(递归层数最小为log?n)
最好 = O(log?n) 最坏 = O(n) 若每一次选中的“枢轴”可以将待排序序列划分为均匀的两个部分,则递归深度最小,算法效率最高;
若初始序列本就有序或者逆序,则快速排序的性能最差;
快速排序算法优化思路: 尽量选择可以把数据中分的枢轴元素
选头、中、尾三个位置的元素,取中间值作为枢轴元素; 随机选一个元素作为枢轴元素; 快速排序使所有内部排序算法中平均性能最优的排序算法;
稳定性: 不稳定;
8.4选择排序
思想:每一趟在待排序元素中选取关键字最小(或最大)的元素加入有序子序列;
8.4.1简单选择排序
n个元素的简单选择排序需要n-1趟处理;
void swap(int &a, int &b){
int temp = a;
a = b;
b = temp;
}
void SelectSort(int A[], int n){
for(int i=0; i<n-1; i++){
int min = i;
for(int j=i+1; j<n; j++)
if(A[j]<A[min]) min = j;
if(min!=i)
swao(A[i],A[min]);
}
}
- 算法效率分析
空间复杂度 = O(1) 无论有序、逆序、乱序,都需要n-1的处理,总共需要对比关键字 (n-1)+(n-2)+…+1 = n(n-2)/2 次,元素交换次数 < n-1; 时间复杂度 = O(n2) 稳定性: 不稳定 适用性: 既可以用于顺序表,也可以用于链表;
8.4.2堆排序
- 什么是“堆(Heap)”?
可理解为顺序存储的二叉树,注意
可以将堆视为一棵 完全二叉树 (?)
可以将堆视为一棵 二叉排序树 (?)
大根堆:完全二叉树中,根 ≥ 左、右 小根堆:完全二叉树中,根 ≤ 左、右
- 如何基于“堆”进行排序
基本思路:每一趟在待排序元素中选取关键字最小(或最大)的元素加入有序子序列,堆顶元素的关键字最大或最小 (以下以大根堆为例)
① 将给定初始序列(n个元素),建立初始大根堆:把所有非终端结点 从后往前都检查一遍,是否满足大根堆的要求——根 ≥ 左、右,若不满足,则将当前结点与更大的孩子互换
在顺序存储的完全二叉树中:
非终端结点的编号 i≤?n/2? i 的左孩子 2i i 的右孩子 2i+1 i 的父节点?i/2? 更小的元素“下坠”后,可能导致下一层的子树不符合大根堆的要求,则采用相同的方法继续往下调整 —— 小元素不断“下坠”
② 基于大根堆进行排序:每一趟将堆顶元素加入有序子序列中,堆顶元素与待排序序列中最后一个元素交换后,即最大元素换到末尾,之后该位置就不用改变,即移出完全二叉树(len=len-1),把剩下的待排序元素序列再调整为大根堆;————“一趟处理”
③ 剩下最后一个元素则不需要再调整;
void BuildMaxHeap(int A[], int len){
for(int i=len/2; i>0; i--)
HeadAdjust(A, i, len);
}
void HeadAdjust(int A[], int k, int len){
A[0] = A[k];
for(int i=2*k; i<=len; i*=2){
if(i<len && A[i]<A[i+1])
i++;
if(A[0] >= A[i])
break;
else{
A[k] = A[i];
k=i;
}
}
A[k] = A[0]
}
void swap(int &a, int &b){
int temp = a;
a = b;
b = temp;
}
void HeapSort(int A[], int len){
BuildMaxHeap(A, len);
for(int i=len; i>1; i--){
swap(A[i], A[1]);
HeadAdjust(A,1,i-1);
}
}
8.5归并排序和基数排序
8.5.1 归并排序
归并(Merge):把两个或多个已经有序的序列合并成一个;
k路归并:每选出一个元素,需对比关键字k-1次;
外部排序通常采用归并排序,内部排序一般采用2路归并;
int *B=(int *)malloc(n*sizeof(int));
void Merge(int A[], int low, int mid, int high){
int i,j,k;
for(k=low; k<=high; k++)
B[k] = A[k];
for(i=low, j=mid+1, k=i; i<=mid && j<= high; k++){
if(B[i]<=B[j])
A[k]=B[i++];
else
A[k]=B[j++];
}
while(i<=mid) A[k++]=B[i++];
while(j<=high) A[k++]=B[j++];
}
void MergeSort(int A[], int low, int high){
if(low<high){
int mid = (low+high)/2;
MergeSort(A, low, mid);
MergeSort(A, mid+1, high);
Merge(A,low,mid,high);
}if
}
算法效率分析:
归并排序的比较次数与序列的初始状态无关;
2路归并的“归并树”——倒立的二叉树,树高h,归并排序趟数m = h-1,第h层最多2^(h-1)个结点,则满足n ≤ 2^(h-1),即h-1 = ?log?n?; 结论: n个元素进行2路归并排序,归并趟数 m = ?log?n?
每趟归并时间复杂度为O(n), 算法总时间复杂度为O(nlog?n);
空间复杂度为O(n); (归并排序算法可视为本章占用辅助空间最多的排序算法)
稳定性:归并排序是稳定的
对于N个元素进行k路归并排序,排序的趟数m满足 k^m = N, m = ?logkN?
8.5.2 基数排序
算法效率分析:
空间效率:O?, 其中r为基数,需要的辅助空间(队列)为r; 时间效率:一共进行d趟分配收集,一趟分配需要O(n), 一趟收集需要O?, 时间复杂度为 O[d(n+r)],且与序列的初始状态无关 稳定性:稳定! 基数排序擅长解决的问题 ①数据元素的关键字可以方便地拆分为d组,且d较小; ②每组关键字的取值范围不大,即r较小; ③数据元素个数n较大;
|