绪论
(1)掌握数据结构的基本概念,数据的逻辑结构、存储结构以及二者之间的关系。
(2)掌握计算语句频度和估算算法时间复杂度的方法。
(3)理解算法五个要素的确切含义。
(4)了解抽象数据类型的定义、表示和实现方法。
-
数据结构:相互之间存在一种或者多种特定关系的数据元素的集合。
-
数据结构三要素:逻辑结构、存储结构、运算。
-
逻辑结构:包括线性结构和非线性结构;线性结构包括一般线性表、受限制的线性表(栈、队列、串)、数组。
-
存储结构:包括顺序存储、链式存储、散列存储和索引存储。
-
运算:运算的定义是针对逻辑结构,实现针对存储结构。
-
逻辑结构和物理结构之间的关系:逻辑结构可以独立于存储结构,存储结构不能独立于逻辑结构。
-
算法五要素:有穷性、确定性、可行性、输入、输出。
-
设计链式存储时,不同结点存储空间可以不连续,但结点内存储单元必须连续。
数据元素:数据的基本单位
数据项:构成数据元素、有独立含义、不可分割的最小单位。
数据对象:性质相同的数据元素的集合,是数据的子集。
线性结构
(1)掌握线性表的顺序存储结构和链式存储结构定义及其各种基本运算。
(2)掌握栈的顺序存储结构和链式存储结构以及基本操作的实现。
(3)掌握队列的顺序存储结构和链式存储结构及其基本操作的实现。
(4)了解串的基本概念及其存储结构。
(5)理解稀疏矩阵和特殊矩阵的压缩方法。
(6)理解广义表的基本概念,掌握广义表的特点及基本操作;
(7)掌握数组的存储表示方法和地址计算方法。
-
线性表的特点:
- 元素个数有限
- 元素都是数据元素
- 元素有先后次序
- 元素的数据类型都相同
顺序表——线性表的顺序存储
-
定义:
-
静态分配(使用数组):
typedef struct{ int data[MaxSize]; //使用数组存放数据元素 int length; //顺序表当前的长度 }SqList;
-
动态分配(使用指针):
typedef struct{ int *data; //指示动态数组的指针 int MaxSize; //顺序表的最大容量 int length; //顺序表的当前长度 }SeqList;
-
-
初始化:
-
静态数组:1、将所有元素初始化为0(防止脏数据);2、将初始长度设为0;
void InitList(SqList &L){ //参数为SqList类型,需要& for(int i=0;i<MaxSize;i++) L.data[i]=0; //将顺序表的所有数据元素初始化为0 L.length=0; //将顺序表的初始长度设为0 }
-
动态数组:1、使用malloc函数动态申请内存;2、将初始长度设为0;
void InitList(SeqList &L){ L.data=(int *)malloc(InitSize*sizeof(int)); //使用malloc函数动态申请内存,InitSize为默认最大长度 L.length=0; //当前长度设为0 L.MaxSize=InitSize; //默认最大长度 }
动态增加数组长度:
- 先将数组中的元素暂存到指针p;
- 重新申请内存;
- 将数据复制到新的内存中;
- 修改顺序表最大容量;
- 释放指针p;
void IncreaseList(SqlList &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; free(p); }
-
-
插入:首先判断存储空间是否已满,若已满返回false;未满情况下检查元素插入位置是否合法(i<1||i>L.length+1视为不合法);在合法的情况下进行元素插入:将插入位置i以及之后的元素依次向后移动一个单位,并将元素e插入位置i,最后线性表长度+1。
bool ListInsert(SqlList &L,int i,int e){ if(L.length>=MaxSize) return false; if(i<1||i>L.length+1) return false; for(int j=L.length;j>=i;j--) L.data[j]=L.data[j-1]; L.data[i-1]=e; L.length++; return true; }
时间复杂度:
- 最好:O(1)
- 最坏和平均:O(n)
-
删除:首先判断删除元素位置是否合法,如果合法将被删元素的值赋给e,然后将被删元素位置后面的所有元素向前移动一位,最后线性表长度-1。
bool ListDelete(SqList &L,int i,int e){ if(i<1||i>L.length) return false; e=L.data[i-1]; for(int j=i,j<L.length;j++) //因为最后一个元素没有后继,所以j不能=L.length L.data[j-1]=L.data[j]; L.length--; return false; }
时间复杂度:
- 最好:O(1)
- 最坏和平均:O(n)
-
按值查找:查找第一个值为e的元素
int LocateElem(SqList L,int e){ for(int i=0;j<L.length;j++){ if(L.data[i]==e) return i+1; } return 0; }
时间复杂度:
- 最好:O(1)
- 最坏和平均:O(n)
单链表
-
定义:包括数据域和指针域,指针域是struct LNode类型
typedef struct LNode{ int data; struct LNode *next; //指针域指向下一个结点,所以是 struct LNode类型 }LNode,*LinkList; //*LinkList用于表示这是一个指向 struct LNode类型的指针
-
初始化:
-
带头结点:给头结点分配内存
bool InitList(LinkList &L){ L=(LNode *)malloc(sizeof(LNode)); if(L==NULL) return false; L->next=NULL; return true; }
-
不带头结点:直接将L设为NULL
bool InitList(LinkList &L){ L=NULL; //空表,暂时没有任何结点 return true; }
-
-
头插法建立单链表:
-
创建头结点L
-
输入结点的值
-
创建新结点*s
-
将结点的值赋予新结点
-
将新结点插入到单链表头部
LinkList List_HeadInsert(LinkList &L){ int x; LNode *s; L=(LinkList)malloc(sizeof(LNode)); L->next=NULL; scanf("%d",&x); while(x!=999){ s=(LNode *)malloc(sizeof(LNode)); s->data=x; s->next=L->next; L->next=s; scanf("%d",&x); } return L; }
-
-
尾插法建立单链表(与头插法相比多了尾指针*r,用于始终指向最后一个结点)
-
创建头结点
-
输入结点的值后进入while循环
-
创建新结点*s,尾指针r
-
将结点的值赋予新结点
-
将新结点插入到单链表尾部
LinkList List_TailInsert(LinkList &L){ int x; LNode *s,*r=L; L=(LinkList)malloc(sizeof(LNode)); scanf("%d",&x); while(x!=999){ s=(LNode *)malloc(sizeof(LNode)); s->data=x; r->next=s; r=s; scanf("%d",&x); } r->next=NULL; return L; }
-
-
按位查找(p初始指向头结点)
LNode *GetElem(LinkList L,int i){ int j=0; //j代表当前扫描到第几个结点 LNode *p; //p代表当前扫描到的结点 p=L; if(i<0) return NULL; while(p!=NULL&&j<i){ p=p->next; j++; } return p; }
-
按值查找(p初始指向第一个结点)
LNode *LocateElem(LinkList L,int e){ LNode *p=L->next; //p指向第一个结点 while(p!=NULL&&p->data!=e){ p=p->next; } return p; }
-
指定结点后插操作
bool InsertNextNode(LNode *p,int e){ if(p=NULL) return false; LNode *s=(LNode *)malloc(sizeof(LNode)); if(s=NULL) return false; s->data=e; s->next=p->next; p->next=s; return true; }
-
插入,在位置i插入结点e
bool ListInsert(LinkList &L,int i,int e){ if(i<1) return false; LNode *p=GetElem(L,i-1); //找到第i-1个结点 InsertNextNode(p,e); //在第i-1个结点后面插入e return true; }
-
指定结点前插操作:先把e插入到p的后面,然后再交换二者的数据域
bool InsertPriorNode(LNode *p,int 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->data=p->data; p->data=e; return true; }
-
按位删除,删除表L中第i个位置的元素。(核心:找到第i-1个结点)
bool ListDelete(LinkList &L,int i,int e){ LNode *p=GetElem(L,i-1); //找到第i-1个结点 if(p==NULL) return false; if(p->next==NULL) return false; LNode *q=p->next; e=q->data; p->next=q->next; free(q); return true; }
-
删除指定结点p。核心:将p后面结点的数据域复制到p中,再将后面的结点删除
bool DeleteNode(LNode *p){ if(p==NULL) return false; LNode *q=p->next; p->data=p->next->data; p->next=q->next; free(q); return true; }
双链表
-
定义
typedef struct DNode{ int data; struct DNode *prior,*next; }DNode,*DLinkList;
-
初始化
bool InitDLinkList(DLinkList &L){ L=(DNode *)malloc(sizeof(DNode)); if(L==NULL) return false; L->next=NULL; L->prior=NULL; return true; }
-
插入(在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; p->next=s; s->prior=p; return true; }
-
删除(删除p的后继结点)
bool DeleteNextDNode(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; }
栈
-
特点:先进后出
-
n个不同元素进栈,出栈元素不同的排列个数为:
-
顺序存储
-
定义
typedef struct{ int data[MaxSize]; int top; }SqStack;
-
初始化(使栈顶指针指向-1)
void InitStack(SqStack &S){ S.top=-1; }
-
判断栈空(栈顶指针指向-1为空)
bool StackEmpty(SqStack S){ if(S.top==-1) return true; else return false; }
-
判断栈满(栈顶指针=MaxSize-1)
bool StackFull(SqStack S){ if(S.top==MaxSize-1) return true; else return false; }
-
入栈,先判断栈满;top指针先加1,然后再入栈。
bool Push(SqStack &S,int x){ if(S.top==MaXSize-1) return false; S.top++; S.data[S.top]=x; return true; }
-
出栈,先判断栈空;站内元素先出栈,top指针再减1。
bool Pop(SqStack &S,int &x){ if(S.top==-1) return false; x=S.data[S.top]; S.top--; return true; }
-
读取栈顶元素,先判断栈空。
bool GetTop(SqStack S,int &x){ if(S.top==-1) return false; x=S.data[S.top]; return true; }
-
-
共享栈:将两个栈底分别设在共享空间的两端,两个栈顶向共享空间的中间延申。
-
栈空:
0号栈为空:S.top==-1; 1号栈为空:S.top==MaxSize;
-
栈满:
top1-top0==1;
-
栈的所有操作时间复杂度都是O(1) 。
函数调用时,系统要用栈保存必要的信息。
-
链式存储
-
栈的链式存储的基本操作本质上对应单链表头插法的基本操作,把单链表的头结点看作是栈顶,那么入栈操作就是头插法建立单链表的操作;出栈操作就是从头结点后面的位置进行删除结点的操作,读取栈顶元素就是读取头结点后面的第一个元素。
-
优点:便于多个栈共享存储空间提高效率,不存在栈满情况。
-
定义
typedef struct Linknode{ int data; struct Linknode *next; }Linknode,*LiStack;
-
初始化
bool InitStack(LiStack &S){ S=(Linknode *)malloc(sizeof(Linknode)); if(S==NULL) return false; S->next=NULL; return true; }
-
入栈
bool Push(LiStack &S,int x){ Linknode *s; s=(Linknode *)malloc(sizeof(Linknode)); if(s==NULL) return false; s->data=x; s->next=S->next; S->next=s; return true; }
-
出栈(先判断栈空)
bool Pop(LiStack &S,int x){ if(S->next==NULL) return false; Linknode *p; p=S->next; x=p->data; S->next=p->next; free(p); return true; }
-
读取栈顶元素
bool GetTop(LiStack S,int x){ if(S->next==NULL) return false; x=S->next->data; return true; }
-
-
栈在递归中的应用:每进入一层递归,将递归所需要的信息压入栈顶,每退出一层递归,从栈顶弹出相关递归信息。
栈的应用还包括进制转换和迷宫求解。
-
进制转换
-
十进制转二进制:除2倒取余; 例如:9(十进制)→1001(二进制)
-
十进制转八进制:除8倒取余; 例如:796(十进制)→1434(八进制)
-
十进制转十六进制:除16倒取余; 例如:796(十进制)→31c (十六进制)
-
八进制、十六进制转换成二进制:八进制数的一位是二进制数的三位,十六进制数的一位是二进制数的四位
-
队列
-
特点:先进先出
-
队列中元素的个数:(Q.rear-Q.front+MaxSize)%MaxSize
-
顺序存储
1、使用顺序存储的方式定义队列时,使用数组存储队列元素,然后声明两个int类型的指针——rear和front,分别指向队尾元素的下一个位置和队头元素的位置。
2、初始化队列时,队列的首尾指针都指向0 。
3、当队列为空时,队尾和队头指针指向同一个位置(不一定等于0)。
4、队满条件:(Q.rear+1)%MaxSize==Q.front
5、执行入队操作,首先需要判断队满,然后先将入队元素放入队尾指针指向的位置,再把队尾指针向后移动(Q.rear=(Q.rear+1)%MaxSize)。
6、执行出队操作,首先需要判断队空,然后把队头指针向后移动(Q.front=(Q.front+1)%MaxSize)。-
定义
typedef struct{ int data[MaxSize]; int front,rear; }SqQueue;
-
初始化
void InitQueue(SqQueue &Q){ Q.front=Q.rear=0; }
-
判断队空
bool QueueEmpty(SqQueue Q){ if(Q.rear==Q.front) return true; else return false; }
-
判断队满
bool QueueFull(SqQueue Q){ if(Q.front==(Q.rear+1)%MaxSize) return true; else return false; }
-
入队
bool EnQueue(SqQueue &Q,int x){ if(Q.front==(Q.rear+1)%MaxSize) return false; Q.data[Q.rear]=x; Q.rear=(Q.rear+1)%MaxSize; return true; }
-
出队
bool DeQueue(SqQueue &Q,int &x){ if(Q.rear==Q.front) return false; x=Q.data[Q.front]; Q.front=(Q.front+1)%MaxSize; return true; }
-
读取队头元素
int GetHead(SqQueue Q,int &x){ if(Q.rear==Q.front) return false; x=Q.data[Q.front]; return x; }
-
-
链式存储
1、首先定义队列结点,包含数据域和指针域;然后定义链式队列,包含队列节点类型的队头和队尾指针。
2、初始化:
带头结点:给头结点分配内存,然后队头和队尾指针指向头结点,同时队头指针的next指向NULL。
不带头结点:队头和队尾指针都指向NULL。
3、入队:
带头结点:先给入队节点分配内存,然后将新节点插入到队尾指针后面,新节点的下一个节点为NULL,最后将队尾指针指向新结点。
不带头结点:先给入队节点分配内存 ,如果队列为空 ,队头和队尾结点都指向新节点,否则将新节点插入到队尾指针后面,最后将队尾指针指向新结点。
4、出队:
带头结点:首先判断队列是否为空,然后定义指针p指向队头指针的下一个结点,如果这是最后一个结点,则front=rear ,最后释放p的内存。
不带头结点:首先判断队列是否为空, 然后定义指针p指向队头指针指向的结点,如果这是最后一个结点,则front=NULL,rear=NULL ,最后释放p的内存。-
定义
typedef struct LinkNode{ //队列结点 int data; struct LinkNode *next; }LinkNode; typedef struct{ LinkNode *rear,*front; }LinkQueue;
-
初始化
-
带头结点(Q.front始终指向头结点)
void InitQueue(LinkQueue &Q){ Q.front=Q.rear=(LinkNode *)malloc(sizeof(LinkNode)); Q.front->next=NULL; }
-
不带头结点
void InitQueue(LinkQueue &Q){ Q.front=Q.rear=NULL; }
-
-
入队
-
带头结点
bool EnQueue(LinkQueue &Q,int x){ LinkNode *s=(LinkNode *)malloc(sizeof(LinkNode)); if(s==NULL) return false; s->data=x; s->next=NULL; Q.rear->next=s; Q.rear=s; return true; }
-
不带头结点
bool EnQueue(LinkQueue &Q,int x){ LinkNode *s=(LinkNode *)malloc(sizeof(LinkNode)); if(s==NULL) return false; s->data=x; s->next=NULL; if(Q.front==NULL){ Q.rear=s; Q.front=s; } else{ Q.rear->next=s; Q.rear=s; } return true; }
-
-
出队
-
带头结点
bool DeQueue(LinkQueue &Q,int &x){ if(Q.rear==Q.front) 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; }
-
不带头结点
bool DeQueue(LinkQueue &Q,int &x){ if(Q.front==NULL) return false; LinkNode *p=Q.front; x=p->data; Q.front=p->next; if(Q.rear==p){ Q.front=NULL; Q.rear=NULL; } free(p); return true; }
-
-
-
栈在括号匹配中的应用(左括号:入栈,右括号:出栈)
bool bracketCheck(char str[],int length){ //str[]用于存放待处理的字符,length表示字符的总个数 SqStack S; InitStack(S); for(int i=0;i<length;i++){ if(str[i]=='['||str[i]=='('||str[i]=='{'){ //左括号入栈 Push(S,str[i]); } else{ //右括号出栈匹配 StackEmpty(S); // 判空 char x; Pop(S,x); //右括号出栈 if(str[i]==')'&&x!='(') return false; if(str[i]==']'&&x!='[') return false; if(str[i]=='}'&&x!='{') return false; } } StackEmpty(S); }
-
队列的应用:层次遍历、缓冲区、页面替换算法。
-
栈的应用:括号匹配、迷宫求解、进制转换、递归
压缩存储和特殊矩阵(P103)
-
二维数组默认从A【1】【1】开始,一维数组默认从B【0】开始。
-
压缩存储:为多个值相同的元素只分配一个存储空间,对0元素不分配存储空间。
-
特殊矩阵:对称矩阵、三角矩阵、三对角矩阵
-
对称矩阵
只存储主对角线和下三角区的元素,将二维数组对称矩阵存储在一维数组中。
-
三角矩阵
- 上三角矩阵:下三角区所有元素均为同一常量,存储主对角线和上三角区的元素以及用一个位置存储下三角区的常量。
- 下三角矩阵:上三角区所有元素均为同一常量,存储主对角线和下三角区的元素以及用一个位置存储上三角区的常量。
-
三对角矩阵
稀疏矩阵
- 定义:矩阵中非0元素的个数相对于矩阵元素的个数来说非常少。
- 方法:将非零元素以及其所在的行和列构成一个三元组。
数组
- 定义:数组是由n个相同类型的数据元素构成的有限序列。每个数据元素称为一个数据元素。
- 每个元素在n个线性关系中的序号称为该元素的下标。
- 下标的取值范围称为数组的维界。
串
- 简单模式匹配算法(主串长度为n,模式串长度为m)
- 时间复杂度:
- 匹配失败最坏:O(mn)
- 匹配失败最好:O(n)
- 匹配成功最好:O(m)
- 时间复杂度:
- KMP算法
- 算法特点:模式匹配时主串指针不会变小。
- 算法过程:
- 利用模式串求出next[]数组。
- 根据next[]数组进行匹配,当第j个元素匹配失败时,查看next数组使模式串指针指向对应位置再次进行匹配。
- 重复以上过程直至成功为止。
- 核心:
- 求next[]数组
- 求nextval[]数组
广义表
-
广义表是线性表的推广,也成为列表。
-
广义表的表示:LS=(a1,a2,a3…an)
ai可以是单个元素,也可以是广义表,分别称为原子和子表。
-
广义表的定义是递归的定义。
-
广义表的运算:
- Head(LS):取表头操作,取出的表头为非空广义表的第一个元素,可以是单原子,也可以是子表;
- Tail(LS):取表尾操作,取出的表尾为去除表头之外,其余元素构成的表,即表尾一定是一个广义表;
树和二叉树
(1)掌握二叉树的基本概念、性质和存储结构。
(2)掌握二叉树的先序、中序、后序和层序遍历方法、算法实现及算法思想的应用。
(3)掌握已知二叉树遍历序列,求解二叉树的方法。
(4)掌握线索二叉树的概念、存储结构及线索化方法。
(5)掌握二叉树、树和森林的转换方法;掌握树和森林的遍历方法。
(6)掌握哈夫曼树的构造和求解哈夫曼编码的方法。
树的基本概念
-
树的定义:树是n个结点的有限集,当n=0时,称为空树。
-
树的特点:
- 树有且仅有一个根结点。
- 树的定义是递归的。
- 除根结点外所有结点有且仅有一个前驱。
- 所有结点可以有0个或者多个后继。
- n个结点的树中有n-1条边。
-
基本术语
-
祖先:根结点到结点K唯一路径上的任意结点称为K的祖先。
子孙
双亲
孩子
兄弟
-
结点的度:结点的孩子个数。
-
树的度:树中结点的最大度数。
-
叶子结点(度为0)——分支结点(度大于0)
-
结点的层次:从树根开始定义,根结点为第一层。
树的深度、高度:树中结点的最大层次。树的根结点深度视为1;
结点的深度:从根结点开始自顶向下逐层累加(根结点深度为0)。
结点的高度:自底向上逐层累加(叶子节点高度为0)。
-
有序树:各结点的子树从左到右是有次序的,不能互换。
-
路径
路径长度:路径上经过边的个数;
树的路径长度:从树根到每个结点的路径长度的总和。
注意:树中的路径是从上到下的,同一双亲的两个孩子之间不存在路径。
-
度为m的树:树中结点的最大度数为m(至少有一个结点的度数为m);
m叉树:每个结点最多有m个孩子;
-
-
树的性质:
-
树中的结点数等于所有的结点度数之和+1;
-
度为m的树中第i层最多有m^(i-1)个结点(当每个结点的度都为m时);
-
高度为h的m叉树的最大结点数(等比数列):
( m h − 1 ) / ( m − 1 ) (m^h-1)/(m-1) (mh−1)/(m−1)
最少结点数:h -
具有n个结点的m叉树的最小高度为(向上取整):
log m ( n ( m − 1 ) + 1 ) \log_m(n(m-1)+1) logm(n(m−1)+1)
-
二叉树的存储结构
-
顺序存储
- 完全二叉树和满二叉树采用顺序存储比较合适,树中结点的序号可以唯一的反映结点之间的关系。
- 顺序存储一定要把二叉树结点编号与完全二叉树对应起来。
- 定义:
struct TreeNode{ int value; bool isEmpty; };
- 初始化
void InitTree(TreeNode t[MaxSize]){ for(int i=0;i<MaxSize;i++){ t[i].isEmpty=true; } }
-
链式存储
- 重要性质:在含有n个结点的二叉链表中,含有n+1个空链域。
- 定义
typedef struct BiTNode{ int data; struct BiTNode *lchild,*rchild; }BiTNode,*BiTree;
- 创建根结点
BiTree createNode(int i){ BiTree root=(BiTree)malloc(sizeof(BiTNode)); root->data=i; root->lchild=NULL; root->rchild=NULL; return root; }
- 插入结点
在二叉树root中插入关键字为data的结点:
- 如果二叉树为空,创建新的根结点;
- 如果二叉树不为空,比较data与root->data的大小,小于插入到左子树,大于插入到右子树;
- 在左/右子树中递归调用insertNode(),直到某结点的左/右子树为空时,为要插入的数据创建根结点;
BiTree insertNode(BiTree root, int data) { if (root == NULL) { return createNode(data); } if (data < root->data) { root->lchild = insertNode(root->lchild, data); } else if (data > root->data) { root->rchild = insertNode(root->rchild, data); } return root; }
-
遍历二叉树(时间复杂度:O(n))
void visit(BiTNode T){ printf("%d",T->data); }
- 先序遍历——根左右
void PreOrder(BiTree T){ if(T!=NULL){ visit(T); PreOrder(T->lchild); PreOrder(T->rchild); } }
- 中序遍历——左根右
void InOrder(BiTree T){ if(T!=NULL){ InOrder(T->lchild); visit(T); InOrder(T->rchild); } }
- 后序遍历——左右根
void PostOrder(BiTree T){ if(T!=NULL){ PostOrder(T->lchild); PostOrder(T->rchild); visit(T); } }
-
中序遍历——非递归算法(利用栈)
核心思想:
- 从根结点开始,沿着根结点的左孩子路径,依次入栈,直到左孩子为空;
- 栈顶元素出栈并访问,若其右孩子为空,继续执行步骤2;
- 若其右孩子不为空,将右子树执行步骤1;
void InOrder2(BiTree T){ InitStack(S); //初始化栈 BiTree p=T; //p是遍历指针,开始指向根结点 while(p!=NULL||!isEmpty(S)){ //当栈或者p指针不为空时循环 if(p!=NULL){ //p指针不为空 Push(S,p); //当前结点入栈 p=p->lchild; } else{ Pop(S,p); visit(p); //栈顶元素出栈并被访问 p=p->rchild; //向右子树走,继续返回循环 } } }
- 先序遍历——非递归算法
与中序遍历的主要区别在于先序遍历是先访问,再入栈;而中序遍历是先入栈再访问;
void PreOrder2(BiTree T){ InitStack(S); BiTree p=T; while(p!=NULL||!=isEmpty(S)){ if(p!=NULL){ visit(p); Push(S,p); p=p->lchild; } else{ Pop(S,p); p=p->rchild; } } }
- 后序遍历——非递归算法
- 沿着根的左孩子依次入栈,直到左孩子为空;
- 读栈顶元素,若栈顶元素的右孩子不为空且未被访问过,将右子树执行步骤1;否则栈顶元素出栈并访问;
void PostOrder(BiTree T){ InitStack(S); BiTree p=T; BiTree r=NULL; //辅助指针r指向最近访问的结点 while(p!NULL||!isEmpty(S)){ if(p!=NULL){ Push(S,p); p=p->lchild; }else{ GetTop(S,p); //读取栈顶元素,非出栈 if(p->rchild!=NULL&&p->rchild!=r){ p=p->rchild; }else{ Pop(S,p); visit(p); r=p; p=NULL; //结点访问完后,重置指针 } } } }
- 层次遍历
void LevelOrder(BiTree T){ InitQueue(Q); //初始化队列 BiTree p; EnOueue(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); } }
-
由遍历序列构造二叉树
- 先序+中序
- 后序+中序
- 层次+中序
核心:找根结点
后序遍历中最后一个结点是根结点;
中序遍历中根结点左边的是左孩子;
先序遍历中第一个结点是根结点;
线索二叉树
- 线索二叉树属于物理结构。
- 引入线索二叉树的目的:加快查找前驱和后继结点的速度。
- 线索化的实质:遍历一次二叉树
- 特点:若无左子树,令lchild指向前驱结点。若无右子树,令rchild指向后继结点;同时引入ltag和rtag指针,当lchild/rchild指向前驱/后继结点时,ltag/rtag=1;
- 定义
typedef struct ThreadNode{
int data;
struct ThreadNode *lchild,*rchild;
int ltag,rtag;
}ThreadNode,*ThreadTree;
- 中序线索化
ThreadNode *pre=NULL;
void visit(ThreadNode *q){
if(q->lchild==NULL){ //左孩子为空,建立前驱线索
q->lchild=pre;
q->ltag=1;
}
if(pre!=NULL&&pre->rchild==NULL){ //右孩子为空,建立后继线索
pre->rchild=q;
pre->rtag=1;
}
pre=q;
}
//中序遍历
void InThread(ThreadTree T){
if(T!=NULL){
InThread(T->lchild);
visit(T);
InThread(T->rchild);
}
}
//线索化二叉树
void CreateInThread(ThreadTree T){
pre=NULL;
if(T!=NULL){
InThread(T); //中序线索化二叉树
if(pre->rchild==NULL) //处理最后一个结点
pre->rtag=1;
}
}
- 先序线索化
ThreadNode *pre=NULL;
void visit(ThreadNode *q){
if(q->lchild==NULL){ //左孩子为空,建立前驱线索
q->lchild=pre;
q->ltag=1;
}
if(pre!=NULL&&pre->rchild==NULL){ //右孩子为空,建立后继线索
pre->rchild=q;
pre->rtag=1;
}
pre=q;
}
//先序遍历
void PreThread(ThreadTree T){
if(T!=NULL){
visit(T);
if(T->ltag==0) //这里使用ltag是为了保证lchild不是前驱线索
PreThread(T->lchild);
PreThread(T->rchild);
}
}
//线索化二叉树
void CreatePrThread(ThreadTree T){
pre=NULL;
if(T!=NULL){
PreThread(T); //中序线索化二叉树
if(pre->rchild==NULL) //处理最后一个结点
pre->rtag=1;
}
}
- 后序线索化
ThreadNode *pre=NULL;
void visit(ThreadNode *q){
if(q->lchild==NULL){ //左孩子为空,建立前驱线索
q->lchild=pre;
q->ltag=1;
}
if(pre!=NULL&&pre->rchild==NULL){ //右孩子为空,建立后继线索
pre->rchild=q;
pre->rtag=1;
}
pre=q;
}
//后序遍历
void PostThread(ThreadTree T){
if(T!=NULL){
PostThread(T->lchild);
PostThread(T->rchild);
visit(T);
}
}
//线索化二叉树
void CreateInThread(ThreadTree T){
pre=NULL;
if(T!=NULL){
PostThread(T); //中序线索化二叉树
if(pre->rchild==NULL) //处理最后一个结点
pre->rtag=1;
}
}
- 中序线索二叉树的遍历
- 找后继:右子树中最左下结点
- 找前驱:左子树中最右下结点
//找到以P为根结点的子树中第一个被访问的结点
ThreadNode *Firstnode(ThreadNode *p){
while(p->ltag==0)
p=p->lchild;
return p;
}
//找到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为根结点的子树中第一个被访问的结点
ThreadNode *Firstnode(ThreadNode *p){
return p;
}
//找到p的后继结点
ThreadNode *Nextnode(ThreadNode *p){
if(p->ltag==0)
return p->lchild;
else
return p->rchild;
}
//遍历
void PreOrder(ThreadNode *T){
for(ThreadNode *p=Firstnode(T);p!=NULL;p=Nextnode(p))
visit(p);
}
- 后序线索二叉树的遍历
- 找后继:
- 如果p是根结点,则没有后继;
- 如果p是右孩子或者p是左孩子但是其双亲没有右孩子,那么其双亲就是后继;
- 如果p是左孩子而且其双亲有右子树,那么其后继是双亲右子树上按后序遍历列出的第一个结点;
- 找后继:
//找到以P为根结点的子树中第一个被访问的结点
ThreadNode *Firstnode(ThreadNode *p){
while(p->ltag==0)
p=p->lchild;
return p;
}
在二叉树中有两个结点m和n,若m是n的祖先,则使用后序遍历可以找到从m到n的路径。
二叉树在线索化后仍然不能有效求解的问题是后序线索二叉树求后序后继。
后序线索二叉树仍然需要栈的支持。
树
-
树的存储结构
-
双亲表示法
- 实现:采用一组连续的存储空间存储每个结点,同时在每个结点中增设一个伪指针,指示其双亲在数组中的位置
- 特点:可以很快找到双亲结点,但是找孩子结点需要遍历整个结构。
- 树和二叉树顺序存储结构的区别:在树的存储结构中,数组下标代表结点的编号,下标中所存内容指示结点之间的关系;二叉树的存储结构中,下标既代表结点的编号,也指示了结点之间的关系。
typedef struct{ //树的结点定义 int data; //数据元素 int parent; //双亲位置域 }PTNode; typedef struct{ //树的类型定义 PTNode nodes[Maxsize]; int n; //结点数 }PTree;
-
孩子表示法
- 实现:将每个结点的孩子结点都用单链表链接起来形成一个线性结构。n个结点有n个孩子链表。
- 特点:寻找双亲需要遍历孩子链表。
-
-
孩子兄弟表示法
- 实现:每个结点包括三部分:数据域、指向第一个孩子的指针,指向第一个兄弟的指针;
- 特点:优点是方便实现树转二叉树和寻找孩子,缺点是寻找双亲比较麻烦。
-
树、森林和二叉树之间的转换(核心:左孩子,右兄弟)
- 树转二叉树:左孩子,右兄弟
- 森林转二叉树:先把森林的每棵树转为二叉树,再把森林的根结点连起来。
- 二叉树转森林:根结点的右孩子分别是每一棵树的根结点;每个结点的左孩子是它的孩子,右孩子是它的兄弟。
-
树和森林的遍历
-
树的遍历
- 深度优先
- 先根遍历:先访问根结点,再依次遍历根结点的每棵子树;
- 后根遍历:先依次遍历根结点的每棵子树,再访问根结点;
- 广度优先:层次遍历
- 深度优先
-
森林的遍历
- 先序遍历森林:依次对每棵树执行先根遍历
- 中序遍历森林:依次对每棵树执行后根遍历
-
树、森林遍历与其对应二叉树遍历之间的关系
树 森林 二叉树 先根遍历 先序遍历 先序遍历 后根遍历 中序遍历 中序遍历
-
哈夫曼树
- 带权路径长度(WPL)
- 结点的带权路径长度:根到该结点的路径长度与该结点权值的乘积。
- 树的带权路径长度:所有叶子结点的带权路径长度之和。
- 哈夫曼树
- 定义:在含有n个带权叶结点的二叉树中,带权路径长度最小的二叉树。
- 构造:每次选取权值最小的两个结点进行结合。
- 特点:所有初始结点最终会成为叶子结点,权值越小的结点到根结点的路径长度越大,一共需要结合n-1次,最终总结点数为2n-1。哈夫曼树中不存在度为1的结点;哈夫曼树不唯一,但是树的带权路径长度一定。
- 哈夫曼编码
- 前缀编码:没有一个编码是另一个编码的前缀,称这样的编码为前缀编码。
- 并查集:双亲表示法存储的树。
图
(1)掌握图的基本概念、性质、邻接矩阵和邻接表存储结构。
(2)掌握图的深度优先搜索和广度优先搜索方法。
(3)掌握图的最小生成树生成方法。
(4)掌握图的最短路径求解方法。
(5)掌握图的拓扑排序和求解关键路径的方法。
注意:图这一章涉及到的所有时间复杂度问题几乎都是(邻接表中有向图与无向图区别除外):
- 邻接表:O(|V|+|E|)
- 邻接矩阵:O(|V|2)
图的基本概念
- 图G由顶点集V和边集E构成,记为G=(V,E)。
- 线性表可以是空表,树可以是空树,但是图不能是空图,顶点集不能为空。
- 有向图:<v,w>:v是弧尾,w是弧头。
- 无向图:(v,w)
- 简单图:没有重复边,不存在顶点到自身的边。
- 完全图:
- 对无向图而言,任何两个顶点之间都存在边,共n(n-1)/2条边;
- 对有向图而言,任何两个顶点之间都存在方向相反的两条弧,共n(n-1)条。
- 子图(V’是V的子集,E’是E的子集)
- 生成子图:子图中包含了原图的所有顶点。
- 并非V和E的任何子集都能构成子图,因为这样的子集可能不是图。
- 连通(无向图)
- 连通:两个顶点之间有路径存在;
- 连通图:任意两个顶点之间有路径存在;
- 连通分量:极大连通子图(子图连通且包含尽可能多的点和边);
- n个顶点组成的连通图,至少有n-1条边,若大于n-1,必有回路;
- 非连通图最多有**(n-1)(n-2)/2**条边;
- 强连通图(有向图)
- 强连通分量:有向图中的极大强连通子图;
- 强连通图最少有n条边;
- 生成树、生成森林
- 顶点的度、入度、出度
- 边的权和网
- 稠密图、稀疏图
- 路径、路径长度和回路
- 简单路径、简单回路
- 距离:最短路径
- 有向树
图的存储和基本操作
-
邻接矩阵法——适合存储稠密图
- 实现:用一维数组存储顶点信息,二维数组存储边的信息;
- 求度:
- 无向图:看行或列
- 有向图:出度看行,入度看列
- 空间复杂度:O(n2),n为顶点数。
- 时间复杂度:O(n)
- 特点:
- 无向图的邻接矩阵是对称矩阵,因此实际存储时只需要存储上/下三角矩阵.
- 无向图第i行/列非0元素个数是顶点的度。
#define MaxNum 100 typedef char VertexType; //定义顶点数据类型 typedef int EdgeType; //定义带权图中边的权值的数据类型 typedef struct{ VertexType Vex[MaxNum]; //顶点表 EdgeType Edge[MaxNum][MaxNum]; //边表 int vexnum,edgenum; //图中顶点数和边数 }MGraph;
-
邻接表法
图的遍历
-
广度优先遍历
bool visited[MaxNum]; //标记数组 void BFS(Graph G,int v){ //从v开始进行广度优先遍历 visit(v); visited[v]=true; EnQueue(Q,v); while(!isEmpty(Q)){ DeQueue(Q,v); for(int w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w)){ if(!visited[w]){ visit(w); visited[w]=true; EnQueue(Q,w); } } } } void BFSTraverse(Graph G){ //广度优先遍历G for(int i=0;i<G.vexnum;i++){ //初始化标记数组 visited[i]=false; } InitQueue(Q); for(int i=0;i<G.vexnum;i++){ if(!visited[i]) BFS(G,i); } }
时间复杂度:
- 邻接表:O(|V|+|E|)
- 邻接矩阵:O(|V|2)
空间复杂度:O(|V|)
-
深度优先遍历(可判断是否存在回路)
- 递归算法
bool visited[MaxNum]; //标记数组 void DFS(Graph G,int v){ //从v开始进行深度优先遍历 visit(v); visited[v]=true; for(int w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w)){ if(!visited[w]) DFS(G,w); } } void DFSTraverse(Graph G){ //深度优先遍历G for(int i=0;i<G.vexnum;i++){ //初始化标记数组 visited[i]=false; } for(int i=0;i<G.vexnum;i++){ if(!visited[i]) DFS(G,i); } }
- 非递归算法
思想:借助栈,使用visited[i]来表示第i个结点是否在栈内或者曾经在栈内
void DFS(Graph &G,int v){ InitStack(S); //初始化栈 for(int i=0;i<G.vexnum;i++){ //初始化标记数组 visited[i]=false; } Push(S,v); visited[v]=true; if(!IsEmpty(S)){ Pop(S,k); visit(k); for(int w=FirstNeighbor(G,k);w>=0;w=NextNeighbor(G,k,w)){ if(!visited[w]){ Push(S,w); visited[w]=true; } } } }
时间复杂度:
- 邻接表:O(|V|+|E|)
- 邻接矩阵:O(|V|2)
空间复杂度:O(|V|)
-
图的应用
- 最小生成树
- 普利姆算法
- 克罗斯卡尔算法
- 最短路径
- BFS算法:两个数组:
- d[]:单源最短路径大小
- path[]:前驱顶点
- 迪杰斯特拉算法:三个数组
- final[]:单源最短路径是否确定
- dist[]:单源最短路径大小
- path[]:前驱顶点
- 弗洛伊德算法(各顶点间的最短路径)矩阵形式
- 无中转点
- 以v0为中转点
- 以v1为中转点
- …
- BFS算法:两个数组:
- 有向无环图
- 拓扑排序
- 关键路径
- 事件最早发生时间ve:从前到后,取最大
- 事件最迟发生时间vl:从后到前,取最小
- 活动最早开始时间e:活动弧起点事件的最早开始时间
- 活动最晚开始时间l:活动弧终点事件的最晚开始时间与活动所需时间之差
- 最小生成树
查找
(1)掌握顺序查找、折半查找与分块查找算法,能对其性能进行分析。
(2)掌握二叉排序树的定义、二叉排序树的构造方法和查找方法。
(3)掌握平衡二叉树的定义、平衡二叉树的构造方法和查找方法。
(4)理解解决冲突的开放地址法与链地址法;掌握哈希表的构造方法;掌握哈希表的查找方法。
基本概念
-
查找:在数据集合中寻找满足某种条件的数据元素的过程。
-
查找表:用于查找的数据集合。
- 静态查找表:不需要动态的插入删除的查找表;例如:顺序、折半、散列查找;
- 动态查找表:需要动态的插入删除的查找表;例如:二叉排序树的查找、散列查找;
-
关键字:数据元素中唯一标识该元素某个数据项的值;
-
平均查找长度:平均比较关键字的次数;——衡量算法效率的主要指标;
顺序查找和折半查找
- 顺序查找
- 算法思想:从头到脚依次查找,既适用于顺序表也适用于链表;
- 时间复杂度:O(n)
- 哨兵的作用:无需判断是否越界,效率更高;
- 一般顺序表的查找:
- ASL成功=(n+1)/2
- ASL失败=n+1
- 有序顺序表的查找:使用判定树
- ASL成功=(n+1)/2
- ASL失败=n/2+n/(n+1)
- 判定树:
- 成功结点的平均查找次数=它自身所在的层数
- 失败结点的平均查找次数=它的父节点所在的层数
- n个结点的判定树共有n+1个失败结点;
-
折半查找——仅适用于有序的顺序表
typedef struct{ int *elem; //动态数组基址 int length; //表长 }SSTable; int Binary_Search(SSTable L,int key){ int high=L.length-1,low=0,mid; while(low<=high){ mid=(low+high)/2; if(L.elem[mid]==key) return mid; else if(L.elem[mid]>key) high=mid-1; else low=mid+1; } return -1; }
-
判定树的构造
- 当low和high之间有奇数个元素时,mid左右元素相等;
- 当low和high之间有偶数个元素时,mid左边元素比右边元素少1;
- 折半查找的判定树一定是平衡二叉树;
- n个结点的判定树共有n+1个失败结点;
- 注意:树高不包括失败结点;
- 成功结点的平均查找次数=它自身所在的层数
- 失败结点的平均查找次数=它的父节点所在的层数
- 树的高度是边的个数;
-
平均查找长度:约等于log2(n+1)-1
-
时间复杂度:O(log2n)
折半查找与二叉排序树的时间性能有时不相同。
-
-
分块查找
- 算法思想:使用索引表,索引表中存放最大关键字和分块区间;在索引表中查找待查记录所属的分块,然后在块内查找。
- 特点:块内无序,块间有序
- 使用顺序查找查索引
- 使用折半查找查索引
注意:当对索引表使用折半查找时,若索引表中不包括目标关键字,则折半查找最终停在low>high,此时要在low所指的分块中查找。
- 查找效率分析(一般只考虑成功的情况)
分块查找的平均查找长度=块间查找的平均查找长度+块内查找的平均查找长度。因此,设索引表一共有b块,每块有s个元素:
- 当使用顺序查找查找索引时,ASL=(s+1)/2+(b+1)/2
- 当使用折半查找查找索引时,ASL=(s+1)/2+log2(b+1)
二叉排序树
- 特点:左<根<右
- 二叉排序树的查找
BSTNode *BST_Search(BiTree T,int key){
while(T!=NULL&&key!=T->data){
if(key>T->data)
T=T->rchild
else if(key<T->data)
T=T->lchild;
}
return T;
}
- 二叉排序树的插入(插入的结点一定是叶子结点)——递归
int BST_Insert(BiTree &T,int key){
if(T==NULL) //原树为空
T=(BiTree)malloc(sizeof(BSTNode));
T->data=key;
T->lchild=T->rchild=NULL;
return 1;
else if(k==T->data) //存在相同关键字,插入失败
return 0;
else if(k<T->data)
return BST_Insert(T->lchild,key);
else
return BST_Insert(T->rchild,key);
}
- 二叉排序树的构造
void Create_BST(BiTree &T,int str[],int n){
T=NULL; //初始化
int i=0;
while(i<n){
BST_Insert(T,str[i]);
i++;
}
}
- 二叉排序树的删除
- 当被删除结点是叶子结点:直接删除该结点;
- 当被删除结点只有左子树或者右子树:令左/右子树代替它的位置;
- 当被删除结点既有左子树,又有右子树:将被删除结点的值改为它的直接后继(或直接前驱)的值,然后删除它的直接后继(或直接前驱)结点从而转化为前两种情况;
- 二叉排序树的查找效率:
- 平均查找长度:根据二叉树自行计算(与树的高度有关)
- 时间复杂度:O(log2n)
平衡二叉树
-
定义:左右子树高度之差不超过1;
-
平衡因子:左右子树的高度差(左-右,可以为负数);
-
平衡二叉树的插入
- 每次调节的都是最小不平衡子树;
- LL平衡旋转(在结点左孩子的左子树上插入结点导致了不平衡):右单旋转
- RR平衡旋转(在结点右孩子的右子树上插入结点导致了不平衡):左单旋转
- LR平衡旋转(在结点左孩子的右子树上插入结点导致了不平衡):先左后右
- RL平衡旋转(在结点右孩子的左子树上插入结点导致了不平衡):先右后左
-
平衡二叉树的删除
- 根据孙子的位置调整平衡:
- 孙子在LL:儿子右单旋
- 孙子在RR:儿子左单旋
- 孙子在LR:孙子先左旋再右旋
- 孙子在RL:孙子先右旋再左旋
- 根据孙子的位置调整平衡:
-
平衡二叉树的查找:与二叉排序树相同
- 最大深度:O(log2n)
- 平均查找长度:O(log2n)
散列查找
-
基本概念
- 散列函数:把查找表中的关键字映射为该关键字对应地址的函数。
- 冲突:多个不同关键字映射到同一地址。
- 同义词:发生碰撞的不同关键字。
- 散列表:建立关键字和存储地址之间的一种直接映射关系。
- 时间复杂度:O(1)
-
散列函数的构造
-
直接定址法
- 描述:直接取关键字的某个线性函数值为散列地址。
- 适用于关键字分布连续的情况。
H ( k e y ) = a ∗ k e y + b H(key)=a*key+b H(key)=a∗key+b
-
除留余数法
-
描述:设散列表表长为m,取一个不大于m但最接近或等于m的质数p,应用以下公式计算散列地址:
H(key)=key%p
-
-
数字分析法
- 描述:选取数码分布均匀的若干位作为散列地址。
-
平方取中法
- 描述:取关键字的平方值的中间几位作为散列地址。
-
-
处理冲突的方法
- 开放定址法
- 线性探测法
- 平方探测法
- 双散列法
- 伪随机序列法
- 拉链法(链地址法)
- 开放定址法
-
散列查找和性能分析
- 装填因子=表中记录数/散列表长度
- 平均查找长度只依赖于装填因子,装填因子越大,发生冲突的可能性越大。
排序
(1)掌握插入类排序算法:直接插入排序,折半插入排序、希尔排序。
(2)掌握交换类排序算法:冒泡排序,快速排序。
(3)掌握选择类排序算法:简单选择排序,堆排序。
(4)理解归并排序算法和基数排序算法。
(5)掌握各种排序方法的特点,能够对各种排序算法进行评价,并能加以灵活应用。
基本概念
- 内部排序:排序期间元素全部存放在内存中的排序。
- 外部排序:排序期间元素无法全部存放在内存中。例如:拓扑排序
- 算法评价指标:
- 时间复杂度
- 空间复杂度
- 稳定性
插入排序
-
直接插入排序
- 每次插入一个元素,并对已插入的元素进行排序。
- 空间复杂度:O(1)
- 时间复杂度:
- 最坏/平均:O(n2)
- 最好:O(n)
- 稳定性:稳定
- 适用性:既适用于顺序存储,也适用于链式存储。
大多数排序算法仅适用于顺序存储
void InsertSort(int A[],int n){ //对n个元素进行直接插入排序
int i,j;
for(i=2;i<=n;i++){ //A[0]位置当“哨兵”,A[1]不需要排序,依次将A[2]~A[n]插入
if(A[i]<A[i-1]){
A[0]=A[i]; //将A[i]放到哨兵位置
for(j=i-1;A[0]<A[i];j--){ //从A[j]开始向前依次与A[0]比较
A[j+1]=A[j]; //如果A[j]比A[0]大,向后移动
}
A[j+1]=A[0];
}
}
}
-
折半插入排序(直接插入排序优化)
- 思路:先用折半查找找到应该插入的位置,然后再移动元素。
先将待插入排序的元素放到A[0]的位置,然后根据已有序列查找插入的位置(当low>high时,从A[low]开始后面所有的元素向后移动一位,然后将A[0]放到A[low]所指的位置;当A[mid]=A[0]时,为了保证稳定性,应该在mid右边寻找插入位置。
- 折半插入排序只是比较关键字的次数少了,但是移动次数基本没有改变,所以时间复杂度不变。
- 空间复杂度:O(1)
- 时间复杂度:
- 最坏/平均:O(n2)
- 最好:O(n)
- 稳定性:稳定
- 适用性:既适用于顺序存储,也适用于链式存储。
-
希尔排序
-
先追求表中元素部分有序,然后再逐渐逼近全局有序。
-
组内排序使用直接插入排序。
-
空间复杂度:O(1)
-
时间复杂度:
- 最坏/平均:O(n2)
- 最好:O(n)
-
稳定性:不稳定
-
适用性:希尔排序仅适用于顺序存储。
-
交换排序
-
冒泡排序
- 思路:每次将待排序元素中最小的元素交换到前面,每次都有一个元素确定最终位置,即从后往前,两两比较相邻元素的值,若为逆序,则交换它们的位置。
void swap(int &a,int &b){ int teap=a; a=b; b=teap; } 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[i-1],A[i]); flag=true; } if(flag==false) return } }
- 空间复杂度:O(1)
- 时间复杂度:
- 最坏/平均:O(n2)
- 最好:O(n)
- 稳定性:稳定
- 适用性:既适用于顺序存储,也适用于链式存储。
-
快速排序
- 思路:首先设置两个指针low和high,分别指向第一个元素和最后一个元素;将low所指的第一个元素设为基准(本次划分一直以该元素为基准),若high指针所指元素大于基准,则向左移动high指针,当high所指元素小于基准时,将该元素移动到low所指位置,此时high指针不动,low指针向右移动;若low指针所指元素小于基准,则向右移动low指针,当low所指元素大于基准时,将该元素移动到high所指位置,此时low指针不动,向左移动high指针;反复重复以上过程,直至low和high指向同一个位置,此时将基准放到该位置,完成第一次划分,基准左边元素都小于基准,基准右边元素都大于基准;然后分别对左边和右边元素进行划分;
//将待排序序列划分为两个部分 void Partition(int A[],int low,int high){ int pivot=A[low]; //j while(low<high){ while(A[high]>=pivot) high--; A[low]=A[high]; while(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); //划分右子表 } }
- 稳定性:不稳定
- 空间复杂度:O(递归层数)
- 时间复杂度:O(n*递归层数)
递归层数:
- 最好:log2n
- 最坏:n
- 若每次选的基准都将排序序列划分为两个不均匀的部分,则会使算法效率降低;
- 若每次选的基准都将排序序列划分为两个均匀的部分,则算法效率最高;
- 若初始序列有序,快速排序性能最差;
选择排序
-
定义:每次在待排序序列中选择最小(或者最大)的元素加入有序序列。
-
简单选择排序
- n个元素的简单选择排序需要n-1趟处理。
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) swap(A[i],A[min]); } }
- 空间复杂度:O(1)
- 时间复杂度:O(n2)
- 稳定性:不稳定
- 适用性:既适用于顺序存储,也适用于链式存储。
-
堆排序
-
大根堆:L(i)>=L(2i)且L(i)>=L(2i+1)
小根堆:L(i)<=L(2i)且L(i)<=L(2i+1)
-
建立大根堆:根据待排序序列建立逻辑视角的二叉树,从后往前检查非终端节点是否满足大根堆的条件,若不满足,将当前结点与更大的孩子互换;若元素互换破坏了下一层的堆,则采用相同的方法继续调整。
//将以k为根的子树调整为大根堆 void HeadAdjust(int A[],int k,int len){ A[0]=A[k]; for(int i=2*k;i<=len;i=1*2){ if(A[i]<A[i+1]) i++; if(A[0]>=A[i]) break; else{ A[k]=A[i]; //小元素下坠 k=i; } } A[k]=A[0]; } //建立大根堆 void Build(int A[],int len){ for(int i=len/2;i>0;i--){ HeadAdjust(A,i,len); } }
- 堆排序:每次将堆顶元素加入有序子序列(具体操作是将堆顶元素与最后一个元素互换,即大根堆得到的是递增序列)
- 空间复杂度:O(1)
- 时间复杂度:O(nlog2n)
- 建堆时间:O(n)
- 每一趟最多:O(nlog2n)
- 稳定性:不稳定
- 堆的插入删除
- 在n个结点的堆中插入和删除一个元素的时间复杂度为O(log2n)
-
归并排序和基数排序
-
归并排序
-
概念:把两个或者多个已经有序的序列合并成一个。
-
二路归并:每次选出一个小元素需要比较关键字一次;
-
二路归并(手算):
二路归并的归并树(如上图)是一棵倒立的二叉树。
-
时间复杂度:n个元素进行二路归并的归并趟数:O(log2n),每趟的时间复杂度是O(n),总复杂度是O(nlog2n)
-
空间复杂度:O(n)
-
稳定性:稳定
-
-
基数排序