第一章:数据结构绪论
1.1 什么是程序
程序=数据结构+算法
1.2 数据结构的基本概念
1.数据:数据是数据元素的集合。
2.数据元素:数据元素是数据的基本单位,通常作为一个整体进行考虑和处理。一个数据元素可由若干数据项组成,数据项是构成数据元素的不可分割的最小单位。例如,学生记录就是一个数据元素,它由学号、姓名、性别等数据项组成。
3.数据对象:数据对象是具有相同性值的数据元素的集合,是数据的一个子集。
4.数据类型:数据类型是一个值的集合和定义再此集合上的一组操作的总称。
1)原子类型。其值不可再分的数据类型。如bool 和int 类型。
2)结构类型。其值可以再分解为若干成分(分量)的数据类型。
3)抽象数据类型。抽象数据组织及与之相关的操作。
5.数据结构:数据结构是相互之间存在一种或多种特定关系的数据元素的集合。
1.3 数据结构的三要素
基本的目标就是将数据及其逻辑关系存储到计算机的内存中
1.3.1 逻辑结构
是指数据对象中数据元素之间的相互关系
是面向问题的
集合结构
线性结构:数据元素之间是一对一关系
树形结构:数据元素之间是一对多关系
图形结构:数据元素之间是多对多关系
1.3.2 物理结构
是指数据的逻辑结构在计算机中的存储形式,因此也称为 存储结构,
是面向计算机的
顺序存储:把逻辑上相邻的元素存储在物理位置也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
链式存储:逻辑上相邻的元素在物理位置上可以不相邻,借助指示元素存储地址的指针来表示元素之间的逻辑关系。
索引存储:在存储元素信息的同时,还建立附加的索引表,索引表中的每项称为索引项,索引项的一般形式是(关键字,地址)
散列存储:根据元素的关键字直接计算出该元素的存储地址,又称哈希(Hash)存储。
什么是存储器
存储器主要是针对内存而言的,像硬盘,软盘,光盘等外部存储器的数据组织通常用文件结构来描述
1.3.3 数据的运算
运算的定义是针对逻辑结构的,指出运算的功能;
运算的实现是针对存储结构的,指出运算的具体操作步骤。
1.4 算法的基本概念
算法(algorithm)是对特定问题求解步骤的一种描述,它是指令的有限序列,其中的每条指令表示一个或多个操作。
算法的特性:
有穷性、确定性、可行性、输入、输出。
好的算法达到的目标:
正确性、可读性、健壮性、效率与低存储量需求。
1.4.1 算法的时间复杂度
一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数f(n),算法的时间量度记作T(n)=O(n),它表示随问题规模n的增大而增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称时间复杂度。
O(2n + 3) = O(2n)
O(2n^2) = O(n^2)
O(n^3) > O(n^2)
1.4.2 算法的空间复杂度
数据结构——时间复杂度&空间复杂度的区别用法 https://blog.youkuaiyun.com/liu17234050/article/details/104251507
第二章:线性表
2.1 定义
零个或多个数据元素的有限序列
2.1.1 顺序存储结构&链式存储结构
https://blog.youkuaiyun.com/liu17234050/article/details/104251282
2.2 顺序表定义
//顺序表实现---静态分配内存
#include<stdio.h>
#define MaxSize 10 //定义表的最大长度
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;
}
//顺序表的实现——动态分配
#include<stdio.h>
#include<stdlib.h>//malloc、free函数的头文件
#define InitSize 10 //默认的最大长度
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;
}
2.2.1 顺序表的特点:
随机访问 ,可以在O(1)时间内找到第i个元素。
存储密度高,每个节点只存储数据元素
拓展容量不方便
插入、删除操作不方便,需要移动大量元素
2.2.2 顺序表基本操作
- 插入操作 :平均时间复杂度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;
}
- 删除操作:平均时间复杂度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;
}
- 按索引查找(获取L表中第i个位置的值):平均时间复杂度O(1)
ElemType GetElem(SqList L, int i){
// ...判断i的值是否合法
return L.data[i-1]; //注意是i-1
}
- 按值查找:平均时间复杂度O(n)
//在顺序表L中遍历
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;
2.3.2 单链表的两种实现方式
- 不带头结点的单链表
头结点:代表链表上头指针指向的第一个结点,不带有任何数据。
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;
}
Status main(){
LinkList L; //声明一个指向单链表的指针: 头指针
//初始化一个空表
InitList(L);
//...
}
//判断单链表是否为空(带头结点)
bool Empty(LinkList L){
if (L->next == NULL)
return true;
else
return false;
}
- 带头结点和不带头结点的比较:
不带头结点:判空L?=NULL;头指针指向的结点用于存放实际数据;
带头结点:判空L->next?=NULL;头指针指向的头结点不存放实际数据,头结点指向的下一个结点才存放实际数据;
2.3.3 单链表上基本操作的实现
- 按位序插入(带头结点),平均时间复杂度:O(n)
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;
}
- 按位序插入(不带头结点)
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;
}
- 指定结点的前插操作
思想:设待插入结点是s,将s插入到p的前面。我们仍然可以将s插入到*p的后面。然后将p->data与s->data交换,这样既能满足了逻辑关系,又能是的时间复杂度为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;
}
//单链表前插法建立
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;
s->next = L->next;
L->next = s; //将新结点插入表中,L为头指针
scanf("%d", &x);
}
return L;
}
- 单链表尾插建立操作
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;
}
- 按位序删除节点(带头结点)
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;
}
- 指定结点的删除
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)
时间复杂度分析:
最坏,平均时间复杂度:O(n)
最好时间复杂度:删除第一个结点 O(1)
- 单链表的查找
平均时间复杂度O(n)
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指针指向的值
}
- 按值查找
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
}
- 求单链表的长度
遍历,平均复杂度:O(n)
- 链表逆置
算法思想:逆置链表初始为空,原表中结点从原链表中依次“删除”,再逐个插入逆置链表的表头(即“头插”到逆置链表中),使它成为逆置链表的“新”的第一个结点,如此循环,直至原链表为空;
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.4 循环双向链表
单链表和循环单链表的比较:
单链表:从一个结点出发只能找到该结点后续的各个结点;对链表的操作大多都在头部或者尾部;设立头指针,从头结点找到尾部的时间复杂度=O(n),即对表尾进行操作需要O(n)的时间复杂度;
循环单链表:从一个结点出发,可以找到其他任何一个结点;设立尾指针,从尾部找到头部的时间复杂度为O(1),即对表头和表尾进行操作都只需要O(1)的时间复杂度;优点:从表中任一节点出发均可找到表中其他结点。
//定义双链表结点类型
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; //头结点的prior指针永远指向NULL
L->next = NULL; //头结点之后暂时还没有结点
return true;
}
//判断双链表是否为空
bool Empty(DLinklist L){
if(L->next == NULL) //判断头结点的next指针是否为空
return true;
else
return false;
}
2.4 顺序表和链表的比较
2.4.1 逻辑结构
顺序表和链表都属于线性表,都是线性结构
2.4.2 存储结构
顺序表:顺序存储
优点:支持随机存取,存储密度高
缺点:大片连续空间分配不方便,改变容量不方便
链表:链式存储
优点:离散的小空间分配方便,改变容量方便
缺点:不可随机存取,存储密度低
2.4.3 基本操作-增/删
顺序表:插入/删除元素要将后续元素后移/前移;时间复杂度=O(n),时间开销主要来自于移动元素;
链表:插入/删除元素只需要修改指针;时间复杂度=O(n),时间开销主要来自查找目标元素
2.4.4 基本操作-查
顺序表
按位查找:O(1)
按值查找:O(n),若表内元素有序,可在O(log2n)时间内找到
链表
按位查找:O(n)
按值查找:O(n)
第三章 栈和队列
3.1 栈(Stack)
栈是特殊的线性表:只允许在一端进行插入或删- 除操作, 其逻辑结构与普通线性表相同;栈又称为 后进先出(Last In First Out) 的线性表,简称 LIFO 结构
3.1.1 栈的基本操作
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 栈的顺序存储
//顺序栈的定义
#define MaxSize 10 //定义栈中元素的最大个数
typedef struct{
ElemType data[MaxSize]; //静态数组存放栈中元素
int top; //栈顶元素
}SqStack;
//初始化栈
void InitStack(SqStack &S){
S.top = -1; //初始化栈顶指针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; //指针先加1
S.data[S.top] = x; //新元素入栈
/*
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; //栈顶指针减1
return true;
// x = S.data[S.top--];
//只是逻辑上的删除,数据依然残留在内存里
}
//读栈顶元素
bool GetTop(SqStack S, ElemType &x){
if(S.top == -1)
return false;
x = S.data[S.top]; //x记录栈顶元素
return true;
}
【注意】: 也可以初始化时定义 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.1.3 链栈基本操作
//带头结点的链栈基本操作
#include<stdio.h>
struct Linknode{
int data; //数据域
Linknode *next; //指针域
}Linknode,*LiStack;
typedef Linknode *Node; //结点结构体指针变量
typedef Node List; //结点结构体头指针变量
//1. 初始化
void InitStack(LiStack &L){ //L为头指针
L = new Linknode;
L->next = NULL;
}
//2.判栈空
bool isEmpty(LiStack &L){
if(L->next == NULL){
return true;
}
else
return false;
}
//3. 进栈(:链栈基本上不会出现栈满的情况)
void pushStack(LiStack &L, int x){
Linknode s; //创建存储新元素的结点
s = new Linknode;
s->data = x;
//头插法
s->next = L->next;
L->next = s;
}
//4.出栈
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; //结点结构体头指针变量
//1.初始化
void initStack(LiStack &L){
L=NULL;
}
//2.判栈空
bool isEmpty(LiStack &L){
if(L == NULL)
return true;
else
teturn false;
}
//3.进栈
void pushStack(LiStack &L, int x){
Linknode s; //创建存储新元素的结点
s = new Linknode;
s->next = L;
L = s;
}
//4.出栈
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 队列的基本概念
队列是操作受限的线性表,只允许在一端进行插入 (入队),另一端进行删除 (出队)
操作特性:先进先出 FIFO
队头:允许删除的一端
队尾:允许插入的一端
空队列:不含任何元素的空表
3.2.2 队列基本操作
队头指针:指向队头元素
队尾指针:指向队尾元素的下一个位置
3.2.2.1 队列的顺序存储类型
define MaxSize 10; //定义队列中元素的最大个数
typedef struct{
ElemType data[MaxSize]; //用静态数组存放队列元素
//连续的存储空间,大小为——MaxSize*sizeof(ElemType)
int front, rear; //队头指针和队尾指针
}SqQueue;
//初始化队列
void InitQueue(SqQueue &Q){
//初始化时,队头、队尾指针指向0
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; //将x插入队尾
Q.rear = (Q.rear + 1) % MaxSize; //队尾指针加1取模
return true;
}
//循环队列——出队:只能让队头元素出队,删除一个队头元素,用x返回
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; //最近进行的是删除or插入 }SqQueue;
3.2.2.2 队列的链式存储结构
-
定义:队列的链式表示称为链队列,是一个同时带有队头指针和队尾指针的单链表。
链队列:用链表表示的队列,是限制仅在表头删除和表尾插入的单链表。 -
链式队列的基本操作——带头结点
//初始化时,front、rear都指向头结点
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) //也可用 Q.front -> next == NULL
return true;
else
return false;
}
//新元素入队 (表尾进行)
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; //新结点插入到rear结点之后
Q.rear = s; //修改rear指针指向新的表尾结点
}
}
//队头元素出队
bool DeQueue(LinkQueue &Q, ElemType &x){
if(Q.front == Q.rear)
return false; //空队
LinkNode *p = Q.front->next; //p指针指向即将删除的结点 (头结点所指向的结点)
x = p->data;
Q.front->next = p->next; //修改头结点的next指针
if(Q.rear == p) //此次是最后一个结点出队
Q.rear = Q.front; //修改rear指针
free(p); //释放结点空间
return true;
}
- 双端队列
定义:双端队列是指允许两端都可以进行入队和出队操作的队列
双端队列允许从两端插入、两端删除的线性表; 如果只使用其中一端的插入、删除操作,则等同于栈;
输入受限的双端队列:允许一端插入,两端删除的线性表; 输出受限的双端队列:允许两端插入,一端删除的线性表;
- 循环队列
利用一组地址连续的存储单元依次存放队列中的数据元素。因为队头和队尾的位置是变化的。所以:设头、尾指针。
3.3 栈的应用
3.3.1 栈在括号匹配中的应用
用栈实现括号匹配**
> ((())) 最后出现的左括号最先被匹配 (==栈的特性—LIFO==);
>**遇到左括号就入栈;**
>**遇到右括号**,就“消耗”一个左括号 (**出栈**);
匹配失败情况:
>**扫描到右括号且栈空,则该右括号单身**;
>**扫描完所有括号后,栈非空,则该左括号单身**;
左右括号不匹配;
#define MaxSize 10
typedef struct{
char data[MaxSize];
int top;
} SqStack;
//初始化栈
InitStack(SqStack &S)
//判断栈是否为空
bool StackEmpty(SqStack &S)
//新元素入栈
bool Push(SqStack &S, char x)
//栈顶元素出栈,用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;
}
}
return StackEmpty(S); //栈空说明匹配成功
}
3.3.2 栈在表达式求值中的应用
- 中缀表达式 (需要界限符)
运算符在两个操作数中间:
① a + b
② a + b - c
③ a + b - c*d
④ ((15 ÷ (7-(1+1)))×3)-(2+(1+1))
⑤ A + B × (C - D) - E ÷ F
- 后缀表达式 (逆波兰表达式)
运算符在两个操作数后面:
① 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;
注意: 先出栈的是“右操作数”
中缀表达式的计算(用栈实现)
两个算法的结合: 中缀转后缀 + 后缀表达式的求值
初始化两个栈,操作数栈 和运算符栈
若扫描到操作数,压人操作数栈
若扫描到运算符或界限符,则按照“中缀转后缀”相同的逻辑压入运算符栈 (期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈项元素并执行相应运算,运算结果再压回操作数栈)
第四章:数组和广义表
4.1 数组(Arrays)
数组是一种特殊的线性表存储结构,其特殊在于表中的元素本身也是一种线性表,内存连续,根据下标在O(1)时间读/写任何元素
4.1.1 数组的顺序存储:行优先顺序和列优先顺序
行优先/列优先存储优点:实现随机存储
起始地址:LOC
M行N列的二维数组 b[M][N] 中,b[i][j]的存储地址:
行优先存储: LOC + (i×N + j) × sizeof(ElemType)
列优先存储:LOC + (j×M + i) × sizeof(ElemType)
4.1.2 特殊矩阵的存储–稀疏矩阵
设在 m * n 的矩阵中有t个非零元素,令c = t /(m*n),当c<=0.05时称为稀疏矩阵。
压缩存储原则:存各非零元的值、行列位置和矩阵的行列数。
- 顺序存储——三元组
- 链式存储——十字链表法
优点:它能够灵活得插入因运算而产生的新的非零元素,删除因运算而产生的新的零元素,实现矩阵的运算。
十字链表中结点的结构示意图:
right:用于链接同一行中的下一个非零元素;
down:用于链接同一列中的下一个非零元素。
在这里插入图片描述
4.2 广义表(generalized tables)
广义表又称列表,也是一种线性存储结构
同数组类似,广义表中既可以存储不可再分的元素,也可以存储广义表
记作:LS=(a1,a2,...,an),其中,LS代表广义表的名称,an表示广义表存储的数据
广义表中每个ai既可以代表单个元素,也可以代表另一个广义表
广义表中存储的单个元素称为"原子",而存储的广义表称为"子表
广义表的存储结构可以分为两种:深度优先顺序和广度优先顺序
在深度优先顺序中:广义表的每个元素都会被递归地处理,直到到达叶子节点
在广度优先顺序中:广义表的每个元素都会被逐层处理,每一层的元素都按照其在该层中的顺序进行处理
广义表的优点是具有较好的灵活性,可以存储任意类型的数据,缺点是访问速度较慢
第五章:串
5.1 串的定义
串(string) 是由零个或多个字符组成的有限序列,又名叫 字符串
5.2 串的逻辑结构VS线性表
串的逻辑结构和线性表很相似
不同之处在于串针对的是字符集,也就是串中的元素都是字符
因此,对于串的基本操作与线性表是有很大差别的
线性表更关注的是单个元素的操作,比如查找一个元素,插入或删除一个元素
但串中更多的是查找子串位置,得到指定位置子串,替换子串等操作
5.3 串的存储结构
//定长顺序存储表示
#define MAXLEN 255 //预定义最大串长为255
typedef struct{
char ch[MAXLEN]; //静态数组实现(定长顺序存储)
//每个分量存储一个字符
//每个char字符占1B
int length; //串的实际长度
}SString;
串长的两种表示法:
方案一:用一个额外的变量length来存放串的长度(保留ch[0]);
方案二:用ch[0]充当length; 优点:字符的位序和数组下标相同;
方案三:没有length变量,以字符’\0’表示结尾(对应ASCII码的0); 缺点:需要从头到尾遍历;
**方案四——最终使用方案:**ch[0]废弃不用,声明int型变量length来存放串的长度(方案一与方案二的结合)
串的链式存储结构
与线性表相似,但由于串结构的特殊性(结构中的每个元素数据都是一个字符)
如果也简单地将每个链结点存储一个字符,就会存在很大的空间浪费
因此,一个结点可以考虑存放多个字符
如果最后一个结点未被占满时,可以使用 “#” 或其他非串值字符补全
串的链式存储结构除了在链接串与串操作时有一定的方便之外
总的来说不如顺序存储灵活,性能也不如顺序存储结构好
5.4 基本操作实现(基于方案四)
#define MAXLEN 255
typedef struct{
char ch[MAXLEN];
int length;
}SString;
// 1. 求子串
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;
}
// 2. 比较两个串的大小
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;
}
// 3. 定位操作
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; //S中不存在与T相等的子串
}
5.5 串的模式匹配
模式匹配:子串的定位操作称为串的模式,它求的是子串(常称模式串)在主串中的位置。
5.5.1 朴素模式匹配算法
int Index(SString S, SString T){
int i=1; //扫描主串S
int j=1; //扫描模式串T
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(n*m)
每个子串都要对比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)
5.5.2 改进的模式匹配算法——KMP算法
- 不匹配的字符之前,一定是和模式串一致的;
- 根据模式串T,求出next数组(只与模式串有关,与主串无关),利用next数组进行匹配,当匹配失败时,主串的指针 i 不再回溯!
- next数组是根据子串求出来的,当前面的字符串已知时如果有重复的,从当前的字符匹配即可。
求next数组
- 作用:当模式串的第j个字符失配时,从模式串的第next[j]继续往后匹配;
- 对于任何模式串,当第1个字符不匹配时,只能匹配下一个子串,因此,next[1] = 0——表示模式串应右移一位,主串当前指针后移一位,再和模式串的第一字符进行比较;
- 对于任何模式串,当第2个字符不匹配时,应尝试匹配模式串的第一个字符,因此,next[2] = 0;
例:对于串 T = ‘abaabc’
next数组的求法:
next数组第一二位一定为0,1
从第三位开始,将前一位与其next值对应的内容进行比较,
如果相等,则该位的next值就是前一位的next值加上1;
如果不等,向前继续寻找next值对应的内容来与前一位进行比较,
直到找到某个位上内容的next值对应的内容与前一位相等为止,
则这个位对应的值加上1即为需求的next值;
如果找到第一位都没有找到与前一位相等的内容,那么求解的位上的next值为1。
利用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)
第六章:树(Tree)
6.1 树的定义
树是 n ( n >= 0 )个结点的有限集
当n = 0时称为空树
树 其实也是一种递归的实现,即树的定义之中还用到了树的概念
1.基本术语
结点之间的关系描述
祖先、子孙、双亲、兄弟…结点
路径、路径长度
2. 结点、树的属性描述
2.1. 结点的层次(深度)——从上往下
2.2. 结点的高度——从下往上
2.3. 树的高度——总共多少层
2.4. 结点的度——有几个孩子
2.5. 树的度——各结点的度的最大值
2.6有序树、无序树
2.7森林
6.2 二叉树
6.2.1 定义
二叉树是n(n>=0)个结点的有限集,它或者是空集(n=0),或者由一个根结点及两颗互不相交的分别称作这个根的左子树和右子树的二叉树组成。
五种形态:
A:空二叉树 B:只有一个跟结点 C:根结点只有左子树 D:根结点只有右子树
E:根结点既有左子树又有右子树
6.2.2 二叉树性质
二叉树的性质:
性质1:在二叉树的第i层上至多有2^(i-1)个结点(i>1)。
性质2:深度为k的二叉树至多有2^k-1个结点(k>=1)。
性质3:对任何一颗二叉树T,如果其叶子数为n0,度为2的结点数为n2,则n0=n2+1.
性质4: 具有n个结点的完全二叉树的深度为(log2N)+1。
注意:二叉树不是树的特殊情况,它们是两个概念。
6.2.3 几种特殊的二叉树
满二叉树:一颗深度为k且有2^k-1个结点的二叉树称为满二叉树。每一层上的结点数都达到最大。叶子全部在最低层。
完全二叉树:结点编号和位置与满二叉树的相同
二叉排序树
平衡二叉树
6.2.4 二叉树的存储结构
- 顺序存储
#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;
6.2.5 二叉树遍历【前序-中序-后序-层序遍历-非递归前序遍历】
前序-中序-后序-层序遍历 https://blog.youkuaiyun.com/liu17234050/article/details/104256408
//非递归前序遍历
void NRPreOrder(BiTree T)
{
SqStack S;
InitStack(&S); // 初始化栈
BiTree p = T;
while (p || !IsEmpty(S))
{
if (p)
{
printf("%c\n", p->data);
Push(&S, p);
p = p->lchild;
}
else
{
Pop(&S, &p);
p = p->rchild;
}
}
}
6.2.6 二叉树基本操作
// 前序序列建立
// void CreatePreBiTree(BiTree *T)
// {
// TElemType ch;
// scanf(" %c", &ch);
// if (ch == '#') // 用'#'表示空树
// *T = NULL;
// else
// {
// *T = (BiTree)malloc(sizeof(BiTNode));
// (*T)->data = ch; // 生成根结点
// CreatePreBiTree(&(*T)->lchild); // 构造左子树
// CreatePreBiTree(&(*T)->rchild); // 构造右子树
// }
// }
// // 后序序列建立
// void CreatePostBiTree(BiTree &root, TElemType arr[])
// {
// static int count = strlen(arr); // 保持count不改
// char item = arr[count - 1];
// count--;
// if (item == '#')
// {
// root = NULL;
// }
// else
// {
// root = new BiTNode;
// root->data = item;
// CreatePostBiTree(root->rchild, arr); // 建立右子树
// CreatePostBiTree(root->lchild, arr); // 建立左子树
// }
// }
// // 中序和前序共同建立
// BiTree CreateBiTreeByInOrder(char pre[], char in[], int inStart, int inEnd, int *preIndex)
// {
// if (inStart > inEnd) // 检查中序序列范围是否有效
// {
// return NULL; // 空树
// }
// char currentChar = pre[*preIndex]; // 获取先序当前字符
// (*preIndex)++;
// BiTree node = (BiTree)malloc(sizeof(BiTNode));
// if (node == NULL)
// {
// printf("内存分配失败.\n");
// exit(1);
// }
// node->data = currentChar; // 存当前节点
// node->lchild = node->rchild = NULL; // 初始化左右孩子为空
// if (inStart == inEnd)
// {
// return node; // 若只有一个元素说明current没有孩子,直接返
// }
// int inIndex;
// for (inIndex = inStart; inIndex <= inEnd; inIndex++)
// {
// if (in[inIndex] == currentChar) // 在中序中查找当前根,左边是左子
// {
// break;
// }
// }
// node->lchild = CreateBiTreeByInOrder(pre, in, inStart, inIndex - 1, preIndex);
// node->rchild = CreateBiTreeByInOrder(pre, in, inIndex + 1, inEnd, preIndex);
// return node;
// }
// Status CreateBiTree(BiTree *BT)
// {
// char pre[100], in[100];
// // 输入前序和中序遍历序列
// printf("Input PreOrder(不带'#'): ");
// scanf("%s", pre);
// printf("Input InOrder(不带'#'): ");
// scanf("%s", in);
// int preIndex = 0; // 前序序列的索引
// *BT = CreateBiTreeByInOrder(pre, in, 0, strlen(in) - 1, &preIndex);
// return 1;
// }
// 交换左右子树
// void SwapBiTree(BiTree BT)
// {
// if (BT)
// {
// BiTree temp = BT->lchild;
// BT->lchild = BT->rchild;
// BT->rchild = temp;
// SwapBiTree(BT->lchild);
// SwapBiTree(BT->rchild);
// }
// }
// // 计算树的深度
// int TreeDepth(BiTree BT)
// {
// if (!BT)
// return 0;
// int leftDepth = TreeDepth(BT->lchild);
// int rightDepth = TreeDepth(BT->rchild);
// return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;
// }
// // 计算叶子结点数
// int LeafNumber(BiTree BT)
// {
// if (!BT)
// return 0;
// if (!BT->lchild && !BT->rchild)
// return 1;
// return LeafNumber(BT->lchild) + LeafNumber(BT->rchild);
// }
// // 计算单分支结点数
// int SingleBranch(BiTree BT)
// {
// if (!BT)
// return 0;
// int count = 0;
// if ((BT->lchild && !BT->rchild) || (!BT->lchild && BT->rchild))
// {
// count = 1;
// }
// return count + SingleBranch(BT->lchild) + SingleBranch(BT->rchild);
// }
// // 计算双分支结点数
// int DoubleBranch(BiTree BT)
// {
// if (!BT)
// return 0;
// int count = 0;
// if (BT->lchild && BT->rchild)
// {
// count = 1;
// }
// return count + DoubleBranch(BT->lchild) + DoubleBranch(BT->rchild);
// }
// 判断两棵树是否相等
// bool Equal(BiTree T1, BiTree T2)
// {
// if (!T1 && !T2)
// return true;
// if (!T1 || !T2)
// return false;
// return T1->data == T2->data && Equal(T1->lchild, T2->lchild) && Equal(T1->rchild, T2->rchild);
// }
6.3 线索二叉树
//线索二叉树结点
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; // 左、右线索标志
}ThreadNode, *ThreadTree;
tag == 0: 指针指向孩子
tag == 1: 指针是“线索”
6.4 哈夫曼树
6.4.1 定义
给定n个权值作为n个叶子结点
构造一棵二叉树,若树的带权路径长度达到最小,则这棵树被称为哈夫曼树
6.4.2 哈夫曼编码
6.4.3 树的带权路径长度
定义:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL
例子:示例中,树的WPL= 1*100 + 2*50 + 3*20 + 3*10 = 100 + 100 + 60 + 30 = 290
第七章:图(Graph)
7.1 图的定义
由顶点的有穷非空集合和顶点之间边的集合组成
通常表示为:G(V,E)
G 表示一个图
V 是图G中的顶点的集合
E 是图G中边的集合
7.2 图的存储结构
7.2.1 邻接矩阵
#define MAX_VERTEX_NUM 20
typedef enum
{
DG,
DN,
AG,
AN
} GraphKind;
typedef int VRType; // 定点类型
// 弧定义
typedef struct
{
int someInfo;
} InfoType;
// 邻接矩阵的每个元素
typedef struct ArcCell
{
VRType adj; // 顶点关系类型
InfoType *info; // 弧的相关信息指针
} ArcCell, AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];
// 图
typedef struct
{
int vexs[MAX_VERTEX_NUM]; // 顶点数组
AdjMatrix arcs; // 邻接矩阵
int vexnum, arcnum; // 图顶点数和弧数
GraphKind kind;
} MGraph;
7.2.2 邻接表
#define MAX_VERTEX_NUM 20 // 最大顶点个数
typedef enum
{
DG,
DN,
AG,
AN
} GraphKind;
typedef int VertexType; // ALGraph顶点类型
// 边的权值类型
typedef int InfoType;
// 邻接表的弧结点结构
typedef struct ArcNode
{
int adjvex; // 弧指向的顶点
struct ArcNode *nextarc;
InfoType weight;
} ArcNode;
// 顶点的结构
typedef struct VNode
{
VertexType data;
ArcNode *firstarc;
} VNode, AdjList[MAX_VERTEX_NUM];
// 图的结构
typedef struct
{
AdjList vertices; // 顶点数组
int vexnum, arcnum; // 顶点数和弧数
GraphKind kind;
} ALGraph;
7.2.3 联系&区别
7.3 图的遍历
7.3.1 DFS
7.3.2 BFS
7.4 图的连通性问题
7.4.1 利用遍历图的算法求解图的连通性问题
在对无向图进行遍历时, 对于连通图,仅需从图中任一顶点出发,进行深度优先搜索或广度优先搜索,便可访问到图中所有顶点。
对非连通图,则需从多个顶点出发进行搜索,而每一次从一个新的起始点出发进行搜索过程中得到的顶点访问序列恰为其各个连通分量中的顶点集。
7.4.2 最小生成树
7.4.2.1 普里姆算法
7.4.2.2 克鲁斯卡尔算法
7.5 有向无环图及其应用
7.5.1 拓扑排序
AOV-网(Activity On Vertex Network)
在有向图中,用顶点表示活动,用有向边<Vi, Vj>表示活动Vi必先于活动Vj。
AOV-网中,不能出现有向环。如果出现了有向环,则意味着某项活动应以自己作为先决条件。
对给定的AOV网络,必须先判断它是否存在有向环——构造它的拓扑有序序列,使得AOV网络中所有应存在的前驱和后继关系都能得到满足。
构造AOV网络全部顶点的拓扑有序序列的运算即拓扑排序。
如果通过拓扑排序能将AOV网络的所有顶点都排入一个拓扑有序的序列中, 则该网络中必定不会出现有向环。
7.5.2 关键路径
7.6 最短路径—Dijkstra算法
第八章 查找
8.1 静态查找表
8.2 二叉排序树
8.3 哈希表
第九章 内部排序
定义:内部排序(也称内部排序算法)是指利用计算机内部存储器(RAM)进行的排序方法
分类:内部排序算法包括
插入排序:直接插入排序、折半插入排序、希尔排序
交换排序:冒泡排序、快速排序
选择排序:简单选择排序、堆排序
归并排序、基数排序
内部排序的优缺点:
9.1 直接插入排序
直接插入排序是一种简单的排序算法,其工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入
9.2 折半插入排序:
折半插入排序是对直接插入排序的一种改进,通过利用二分查找的方法确定插入的位置,可以减少比较次数
9.3 希尔排序
希尔排序也称为缩小增量排序,其基本思想是将原数据集合分割成若干个子序列
然后对子序列分别进行直接插入排序,逐步减小子序列的间隔,直至整个序列基本有序或只剩一个元素时停止
最后再进行一次直接插入排序
时间复杂度为O(n log n)
9.4 冒泡排序
冒泡排序是一种简单直观的排序算法,其工作原理是通过重复地遍历待排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来
时间复杂度为O(n^2)
9.5 快速排序
快速排序是一种分治的排序算法,通过选定一个比较基准
然后将数组分为两部分,一部分的所有元素都比另一部分的元素小
然后再对这两部分分别进行快速排序,最终整个序列有序
时间复杂度为平均O(n log n),最坏情况O(n^2)
//用第一个元素将待排序序列划分为左右两个部分
int Partition(int A[], int low, int high){
int pivot = A[low]; //用第一个元素作为枢轴
while(low<high){
while(low<high && A[high]>=pivot) --high; //high所指元素大于枢轴,high左移
A[low] = A[high]; //high所指元素小于枢轴,移动到左侧
while(low<high && A[low]<=pivot) ++low; //low所指元素小于枢轴,low右移
A[high] = A[low]; //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); //划分右子表
}
9.6 简单选择排序
简单选择排序是一种简单直观的排序算法,其工作原理是首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。如此重复,直到所有元素均排序完毕
时间复杂度为O(n^2)
//交换
void swap(int &a, int &b){
int temp = a;
a = b;
b = temp;
}
void SelectSort(int A[], int n){ //A从0开始
for(int i=0; i<n-1; i++){ //一共进行n-1趟,i指向待排序序列中第一个元素
int min = i; //记录最小元素位置
for(int j=i+1; j<n; j++) //在A[i...n-1]中选择最小的元素
if(A[j]<A[min]) min = j; //更新最小元素位置
if(min!=i)
swao(A[i],A[min]); //交换
}
}
9.7 堆排序
//对初始序列建立大根堆
void BuildMaxHeap(int A[], int len){
for(int i=len/2; i>0; i--) //从后往前调整所有非终端结点
HeadAdjust(A, i, len);
}
/*将以k为根的子树调整为大根堆
从最底层的分支结点开始调整*/
void HeadAdjust(int A[], int k, int len){
A[0] = A[k]; //A[0]暂存子树的根结点
for(int i=2*k; i<=len; i*=2){ //沿key较大的子结点向下筛选
// i为当前所选根结点的左孩子
//i*=2是为了判断调整后再下一层是否满足大根堆
if(i<len && A[i]<A[i+1]) //判断:当前所选根结点的左、右结点哪个更大
i++; //取key较大的子结点的下标
if(A[0] >= A[i])
break; //筛选结束:i指向更大的子结点
else{
A[k] = A[i]; //将A[i]调整至双亲结点上
k=i; //修改k值,以便继续向下筛选
}
}
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--){ //n-1趟的交换和建堆过程
swap(A[i], A[1]); //堆顶元素和堆底元素交换
HeadAdjust(A,1,i-1); //把剩余的待排序元素整理成堆
}
}
9.8 归并排序
归并排序是一种基于分治策略的排序算法,它将原始数组分割成两个子数组,对每个子数组进行排序,然后将结果合并在一起
这个过程可以递归地进行,直到数组的大小为1
时间复杂度为O(n log n)
//创建辅助数组B
int *B=(int *)malloc(n*sizeof(int));
//A[low,...,mid],A[mid+1,...,high] 各自有序,将这两个部分归并
void Merge(int A[], int low, int mid, int high){
int i,j,k;
for(k=low; k<=high; k++)
B[k] = A[k]; //将A中所有元素复制到B中
for(i=low, j=mid+1, k=i; i<=mid && j<= high; k++){
if(B[i]<=B[j]) //为保证稳定性两个元素相等时,优先使用靠前的那个
A[k]=B[i++]; //将较小值复制到A中
else
A[k]=B[j++];
}//for
//没有归并完的部分复制到尾部,while只会执行一个
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
}
9.9 基数排序
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字段,然后按每个位数分别比较
时间复杂度取决于最大数的位数,通常为O(nk),其中k是最大数字的位数