基本概念和术语
数据:是描述客观事物的符号,是计算机中可以操作的对象,是能被计算机识别,并输入给计算机处理的符号集合
数据元素:是组成数据的,有一定意义的基本单位,在计算机中通常作为整体处理。也被称为记录
数据项:一个数据元素可以由若干个数据项组成,数据项是数据不可分割的最小单位
数据对象:是性质相同的数据元素的集合,是数据的子集
数据结构:是相互之间存在一种或多种特定关系的数据元素的集合
逻辑结构:是指数据对象中数据元素之间的相互关系
- 集合结构:相互之间没有其他关系
- 线性结构:一对一关系
- 树形结构:一对多关系
- 图形结构:多对多关系
物理结构:是指数据的逻辑结构在计算机中的存储形式
顺序存储结构:是把数据元素存放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一致的
链式存储结构:是把数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的
数据类型:是指一组性质相同的值的集合及定义在此集合上的一些操作的总称。
抽象:是指抽取出事物具有的普遍性的本质
抽象数据类型(Abstract Data Type ADT):是指一个数字模型及定义在该模型上的一组操作
抽象数据类型体现了程序设计中问题分解、抽象和信息隐藏的特性
1、算法的时间复杂度
算法的时间复杂度,也就是算法的时间量度,记作:T(n)= O(f(n))
T(n)增长最慢的算法为最优算法
O(n)来体现算法时间复杂度的记法,我们称之为大O记法
其中f(n)是一个表达式
最坏情况运行时间是一种保证,那就是运行时间将不会再坏了。在应用中,这是一种最重要的需求,通常,除非特别指定,我们提到的运行时间都是最坏情况的运行时间
算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作:S(n)= O(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数
2、线性表
定义:线性表(List)零个或多个数据元素的有限序列
-
Data
线性表的数据对象集合为{a1,a2,…,an},每个元素的类型均为DataType,除第一个元素外,每一个元素有且只有一个直接前驱元素,除最后一个元素外,每一个元素有且只有一个直接后继元素。数据元素之间的关系是一对一的关系 -
Operation
InitList(*L):初始化操作,建立一个空的线性表L ListEmpty(L):若线性表为空,返回true,否则返回false ClearList(*L):将线性表清空 GetElem(L,i,*e):将线性表L中的第i个位置元素返回给e LocateElem(L,e):在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中序号表示成功,否则,返回0表示失败 ListInsert(*L,i,e):在线性表L中的第i个位置插入新元素e ListDelete(*L,i,*e):删除线性表L中第i个位置元素,并用e返回其值 ListLength(L):返回线性表L的元素个数
2.1、线性表的存储(存储包括:顺序和链式)
2.1.1、顺序存储结构
定义:指的是用一段地址连续的存储单元依次存储线性表的数据元素
- 初始化线性表
#define MAXSIZE 20 /*存储空间初始化分配量*/ typedef int ElemType; /*ElemType类型根据实际情况而定,这里假设为int*/ typedef struct { ElemType data[MAXSIZE]; /*数组存储数据元素,最大值为MAXSIZE*/ int length; /*线性表当前长度*/ }SqList;
- 获得元素操作
#define OK 1 #define ERROR 0 typedef int Status; Status GetElem(SqList L, int i, ElemType *e) { //判断如果线性表的长度为零,或者获取元素位置小于初始位置, //或大于线性表当前长度,则返回错误 if(L.length == 0 || i < 1 || i > L.length) return ERROR; *e = L.data[i-1];//将要获取的数据放入*e中 return OK; }
- 插入操作
Status ListInsert(SqList *L, int i, ElemType e) { int k; if(L->length == MAXSIZE) /*顺序线性表已满*/ return ERROR; if(i < 1 || i > L->length+1) /*当i不在范围内时*/ return ERROR; if(i <= L->length) /*若插入数据位置不在表尾*/ { /*将要插入位置后数据元素向后移动一位*/ for(k = L->length-1; k >= i-1; k--) L->data[k+1] = L->data[k]; } L->data[i-1] = e; /*将新元素插入*/ L->length++; return OK; }
- 删除操作
Status ListDelete(SqList *L, int i, ElemType *e) { int k; if(L->length == 0) /*线性表为空*/ return ERROR; if(i < 1 || i > L->length) /*删除位置不正确*/ return ERROR; *e = L->data[i-1]; //将删除的元素保存下来 if(i < L->length) /*如果删除不是最后位置*/ { for(k = i; k < L->length; k++)/*将删除位置后继元素前移*/ L->data[k-1]=L->data[k]; } L->length--; return OK; }
线性表顺序存储的优缺点:
- 优点:
1.无须为表示表中元素之间的逻辑关系而增加额外的存储空间
2.可以快速地存取表中任一位置的元素 - 缺点:
1.插入和删除操作需要移动大量元素
2.当线性表长度变化较大时,难以确定存储空间的容量
3.会造成存储空间的“碎片”
2.1.2、链式存储结构
线性表的链式存储结构又称为单链表
头指针与头结点异同:头指针是指向第一个元素的,如果存在头结点则指向头结点
线性表链式存储的初始化
typedef struct Node
{
ElemType data;//存放数据
struct Node *next;//存放下一结点的地址
}Node;
typedef struct Node *LinkList; //定义LinkList
2.2、单链表
定义:n个结点(a1的存储映像)链结成一个链表,即为线性表(a1,a2,…,an)的链式存储结构,因此此链表的每个结点中只包含一个指针域,所以叫单链表
- 单链表的查询
Status GetElem(LinkList L, int i, ElemType *e) { int j; LinkList p; //声明一结点p p = L->next; //让p指向链表L的第一个结点 j = 1; //j为计数器 while(p && j < i)//如果p不为空或者计数器j还没有等于i时,循环继续 { p = p->next; //让p指向下一个结点 ++j; } if( !p || j > i) return ERROR; //第i个元素不存在 *e = p->data; //取第i个元素的数据 return OK; }
- 单链表的插入
Status ListInsert(LinkList *L, int i, ElemType e) { int j; LinkList p, s; p = *L; j = 1; while(p && j < i) //寻找第i个结点 { p = p->next; ++j; } if( !p || j > i) return ERROR; //第i个元素不存在 //动态生成新的结点 s = (LinkList)malloc(sizeof(Node)); s->data = e; s->next = p->next; //将p的后继结点赋值给s的后继 p->next = s; //在将s的后继赋值给p的后继 return OK; }
- 单链表的删除
Status ListDelete(LinkList *L, int i, ElemType *e) { int j; LinkList p,q; p = *L; j = 1; while (p->next && j < i) //遍历寻找第i个元素 { p = p->next; ++j; } if( !(p->next) || j > i) return ERROR; //第i个元素不存在 q = p->next; p->next = q->next; //将q的后继赋值给p的后继 *e = q->data; //将q结点中的数据给e free(q); //释放内存 return OK; }
- 单链表的整表创建–头插法
void CreateListHead(LinkList *L, int n) { LinkList p; int i; srand(time(0)); //初始化随机数种子 *L = (LinkList)malloc(sizeof(Node)); (*L)->next = NULL; //建立一个带头结点的单链表 for( i = 0; i < n; i++) { p = (LinkList)malloc(sizeof(Node)); //生成新结点 p->data = rand()%100+1; //随机生成100以内的数字 p->next = (*L)->next; (*L)->next = p; //插入到表头 } }
- 单链表的整表创建–尾插法
void CreateListTail(LinkList *L, int n) { LinkList p, r; int i; srand(time(0)); //初始化随机数种子 *L = (LinkList)malloc(sizeof(Node));//建立一个带头结点的单链表 r = *L; //r为指向尾部的结点 for( i = 0; i < n; i++) { p = (LinkList)malloc(sizeof(Node)); //生成新结点 p->data = rand()%100+1; //随机生成100以内的数字 r->next = p; //将表尾终端结点指针指向新结点 r = p; //将当前新结点定义为表尾的终端结点 } r->next = NULL; //表示当前链表结束 }
- 单链表的整表删除
Status ClearList(LinkList *L) { LinkList p, q; p = (*L)->next; //p指向第一个结点 while(p) //循环没有到表尾 { q = p->next; free(p); p = q; } (*L)->next = NULL; //头结点指针域为空 return OK; }
总结:若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构。若需要频繁插入和删除时,宜采用单链表结构
2.3、静态链表
定义:数组的元素都是由两个数据域组成,data和cur。每个数组都有两个小标一个数据域data(存放数据元素)和一个游标cur(存放后继元素的下标)。我们用这种数组描述的链表叫做静态链表
-
静态链表存储结构
#define MAXSIZE 1000 //初始化链表的最大长度1000 typedef struct { ElemType data; //数据域 int cur; //游标,为0时无指向 }Component,StaticLinkList[MAXSIZE];
-
模拟分配新空间
int Malloc_SLL(StaticLinkList space) { int i = space[0].cur; //当前数组第一元素的cur存的值 if(space[0].cur) //判断为空则为空链表,不需要重新计算返回备用空闲下标 { space[0].cur = space[i].cur; //由于要拿上一个分量使用,所以存储下一个分量做备用 } return i; }
-
模拟释放删除空间
void Free_SSL(StaticLinkList space, int k) { space[k].cur = space[0].cur; //把第一个元素cur值赋值给要删除的分量cur space[0].cur = k; //把要删除的分量下标赋值给第一个元素的cur }
-
若静态链表L已存在,则返回L中数据元素个数
int ListLength(StaticLinkList L) { int j = 0; int i = L[MAXSIZE - 1].cur; while(i) { i = L[i].cur; j++; } return j; }
-
静态链表的插入
Status ListInsert(StaticLinkList L, int i, ElemType e) { int j, k, m; k = MAX_SIZE-1; //获取最后一个元素的下标 if(i < 1 || i > ListLength(L) + 1) return ERROR; j = Malloc_SLL(L); //获取当前空闲分量下标 if(j) { L[j].data = e; //将数据赋值给此分量的data for(m = 1; m <= i-1; m++) //找到第i个元素之前的位置 k = L[k].cur; L[j].cur = L[k].cur; //将第i个元素之前的cur赋值给新元素cur L[k].cur = j; //把新元素的下标赋值给第i个元素之前的元素cur return OK; } return ERROR; }
-
静态链表的删除
Status ListDelete(StaticLinkList L, int i) { int j, k; if(i < 1 || i > ListLength(L)) return ERROR; k = MAX_SIZE -1; for(j = 1; j <= i-1; j++) k = L[k].cur; j = L[k].cur; L[k].cur = L[j].cur; Free_SSL(L, j); return OK; }
-
优点:在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进在顺序存储结构中的插入和删除操作需要移动大量元素的缺点
-
缺点:没有解决连续存储分配带来的表长难以确定的问题,失去了顺序存储结构随机存取的特性
2.4、循环链表
定义:将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表
2.5、双向链表
定义:是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。所以在双向链表中的结点都有两个指针域,一个指向直接后继,另一个指向直接前驱
3、栈
定义:是限定仅在表尾进行插入和删除操作的线性表
插入和删除的一端称为栈顶,另一端称为栈底,不含任何数据元素的栈称为空栈。栈又称为后进先出(Last In First Out)的线性表,简称LIFO结构
- Data
同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系 - Operation
InitStack(*S):初始化操作,建立一个空栈S DestroyStack(*S):若栈存在,则销毁它 ClearStack(*S):将栈清空 StackEmpty(S):若栈为空,返回true,否则返回false GetTop(S,*e):若栈存在且非空,用e返回S的栈顶元素 Push(*S,e):若栈存在,插入新元素e到栈S中并成为栈顶元素 Pop(*S,*e):删除栈S中栈顶元素,并用e返回其值 StackLength(S):返回栈S的元素个数
3.1、栈的顺序存储
进栈和出栈都在栈顶,栈顶一般由top表示,如果为空栈则top为-1
- 栈的顺序存储的结构定义
typedef int SElemType; typedef struct { SElemType data[MAXSIZE]; int top; //用于栈顶指针 }SqStack;
- 进栈
Status Push(SqStack *S, SElemType e) { if(S->top == MAXSIZE - 1) //栈满 return ERROR; S->top++; //栈顶 S->data[S->top] = e; //将新插入元素赋值给栈顶空间 return OK; }
- 出栈
Status Pop(SqStack *S, SElemType *e) { if(S->top == -1) //栈空 return ERROR; *e = S->data[S->top]; S->top--; return OK; }
3.2、栈的链式存储
栈的链式存储结构,简称为链栈
- 链栈的存储结构
typedef struct StackNode { SElemType data; struct StackNode * next; }StackNode,*LinkStackPtr; typedef struct LinkStack { LinkStackPtr top; int count; }LinkStack;
- 进栈
Status Push(LinkStack *S, SElemType e) { //动态生成一个结点 LinkStackPtr s = (LinkStackPtr)malloc(sizeof(StackNode)); s->data = e; //将新添加的值赋值给新生成的结点 s->next = s->top; //将当前栈顶指针赋值新生成结点next s->top = s; //将当前结点赋值给当前栈顶 s->count++; //链栈总数++ return OK; }
- 出栈
Status Pop(LinkStack *S, SElemType *e) { LinkStackPtr p; if(StackEmpty(*S)) return ERROR; *e = S->top->data; p = S->top; //将栈顶结点赋值给p S->top = S->top->next; //使栈顶指针下移一位,指向后一结点 free(p); //释放结点p S->count--; return OK; }
栈的应用—递归中
栈的应用—四则运算表达式求值
3.3、队列的存储
定义:是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。队列是一种先进先出的线性表,简称FIFO
- ADT队列(Queue)
- Data
同线性表。元素具有相同的类型,相邻元素具有前驱和后继 - Operation
InitQueue(*Q):初始化操作,建立一个空队列Q DestroyQueue(*Q):若队列Q存在,则销毁它 ClearQueue(*Q):将队列Q清空 QueueEmpty(Q):若队列Q为空,返回true,否则返回false GetHead(Q,*e):若队列Q存在且非空,用e返回队列Q的队头元素 EnQueue(*Q,e):若队列Q存在,插入新元素e到队列Q中并成功队尾元素 DeQueue(*Q,*e):删除队列Q中队头元素,并用e返回其值 QueueLength(Q):返回队列Q的元素个数
- endADT
3.3.1、队列顺序存储的不足
插入元素都是从队尾插入所以时间复杂度为O(1),但是删除元素时都要在队头删除元素,为了保持队头始终从零开始,因此每当删除一个元素,都得依次往前运动,时间复杂度为O(n),与线性表的顺序存储结构完全相同
3.3.2、循环队列定义
我们把队列的这种头尾相接的顺序存储结构称为循环队列
- 循环队列的顺序存储结构
typedef int QElemType; typedef struct { QElemType data[MAXSIZE]; int front; //头指针 int rear; //尾指针,若队列不空,指向队列尾元素的下一个位置 }SqQueue;
- 初始化一个空队列Q
Status InitQueue(SqQueue *Q) { Q->front = 0; Q->rear = 0; return OK; }
- 循环队列长度
int QueueLength(SqQueue Q) { return (Q.rear-Q.front+MAXSIZE)%MAXSIZE; }
- 入队列
Status EnQueue(SqQueue *Q, QElemType e) { if((Q->rear+1)%MAXSIZE == Q->front) //队列满的判断 return ERROR; Q->data[Q->rear] = e; //将元素e赋值给队尾 Q->rear = (Q->rear+1)%MAXSIZE; //rear指针向后移一位置 return OK; }
- 出队列
Status DeQueue(SqQueue *Q, QElemType *e) { if(Q->front == Q->rear) //队列空的判断 return ERROR; *e = Q->data[Q->front]; //将队头元素赋值给e Q->front = (Q->front + 1)%MAXSIZE; //front指针向后移一位置 }
3.3.3、队列的链式存储结构
定义:队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,我们把他简称为链队列
- 链队列的结构
typedef int QElemType; typedef struct QNode //结点结构 { QElemType data; struct QNode *next; }QNode, *QueuePtr; typedef struct //队列的链表结构 { QueuePtr front, rear; //队头、队尾指针 }LinkQueue;
- 入队
Status EnQueue(LinkQueue *Q, QElemType e) { QueuePtr s = (QueuePtr)malloc(sizeof(QNode)); if(!s) //存储分配失败 exit(OVERFLOW); s->data = e; s->next = NULL; Q->rear->next = s; //把新结点赋值给原队尾结点 Q->rear = s; //将s设置为当前尾结点 return OK; }
- 出队
Status DeQueue(LinkQueue *Q, QElemType *e) { QueuePtr p; if(Q->front == Q->rear) //如果为空队列 return ERROR; p = Q->front->next; //将要删除的队头结点暂存给P *e = p->data; //获取p中的数据给e Q->front->next = p->next;//将原队头结点后继p->next赋值给头结点后继 if(Q->rear == p) //若队头是队尾,则删除后将rear指向头结点 Q->rear = Q->front; free(p); return OK; }
总结:
- 栈:是限定仅在表尾进行插入和删除操作的线性表
- 队列:是允许在一端进行插入操作,而在另一端进行删除操作的线性表
4、串
定义:串是由零个或多个字符组成的有限序列,又名叫字符串
4.1、串的存储
- ADT串(string)
- Data
串中元素仅由一个字符串组成,相邻元素具有前驱和后继关系 - Operation
StrAssign(T, *chars):生成一个其值等于字符串常量chars的串T StrCopy(T,S):串S存在,由串S复制得串T ClearString(S):串S存在,将串清空 StringEmpty(S):若串S为空,返回true,否则返回false StrLength(S):返回串S的元素个数,即串的长度 StrCompare(S,T):若S>T,返回值>0,若S=T,返回0,若S<T,返回值<0 Concat(T,S1,S2):用T返回由S1和S2联接而成的新串 SubString(Sub,S,pos,len):串S存在,1<= pos <= Strlength(S), 且0 <= len <= StrLength(S)-pos+1,用Sub返回串S的第pos个字符起长 度为len的子串 Index(S,T,pos):串S和T存在,T是非空串,1<= pos <=StrLength(S).若 主串S中存在和串T值相同的子串,则返回它在主串S中第pos个字符串之后 第一次出现的位置,否则返回0 Replace(S,T,V):串S、T和V存在,T是非空串。用V替换主串S中出现的所有 与T相等的不重叠的子串 StrInsert(S,pos,T):串S和T存在,1 <= pos <= StrLength(S) + 1。在串 S的第pos个字符串之前插入串T StrDelete(S,pos,len):串S存在,1 <= pos <=StrLength(S) - len + 1。 从串S中删除第pos个字符串起长度为len的子串
- index的实现算法
int Index(String S, String T, int pos) { int n, m, i; String sub; if(pos > 0) { n = StrLength(S); //得到主串S的长度 m = StrLength(T); //得到子串T的长度 i = pos; while(i <= n-m+1) { SubString(sub, S, i, m); //取主串第i个位置 //长度与T相等子串给sub if(StrCompare(sub,T) != 0) //如果两串不相等 ++i; else return i; //两串相等则返回i值 } } return 0; //若无子串与T相等,返回0 }
当中用到了StrLength、SubString、StrCompare等基本操作来实现
- 串的顺序存储结构:串的顺序存储结构是用一组地址连续的存储单元来存储串中的字符序列的
- 串的链式存储结构:与线性表相似,但结构中每个元素数据是一个字符
- 总结:串的链式存储结构除了在连续串与串操作时有一定方便之外,总的来说不如顺序存储灵活,性能也不如顺序存储结构好
4.2、朴素的模式匹配算法
定义:串的定位操作通常称做串的模式匹配
我们现在来匹配一下主串S=“goodgoogle”中,找到T=“google”这个子串的位置,我将通过以下截图来展示
简单的来说,就是对主串的每一个字符作为子串开头,与要匹配的字符串进行匹配。对主串做大循环,每个字符开头做T的长度的小循环,直到匹配成功或者全部遍历完成为止
- 我们现在用模式匹配的算法Index
- 我们现在假设主串S和要匹配的子串T的长度存在S[0]与T[0]中
- 返回子串T在主串S中第pos个字符之后的位置。若不存在,则函数返回值为0
- T非空,1 <= pos <= StrLength(S)
int Index(String S, String T, int pos) { int i = pos; //i用于主串S中当前位置下标,若pos不为1,则从pos位置开始 int j = 1; //j用于子串T中当前位置下标值 while(i <= S[0] && j <= T[0])//当i<S长度和j<T长度时循环 { if(S[i] == T[j]) //两字母相等则继续 { ++i; ++j; }else{ //指针后退重新开始匹配 i = i - j + 2; //i退回到上次匹配首位的下一位 j = 1; //j退回到子串T的首位 } } if(j > T[0]) return i-T[0]; else return 0; }
4.3、kmp模式匹配算法
由来:是由克努特-莫里斯-普拉特发表的一个模式匹配算法,可以大大避免重复遍历的情况,简称KMP算法
KMP模式匹配算法实现
- 通过计算返回子串T的next数组
void get_next(String T, int *next) { int i, j; i = 1; j = 0; next[1] = 0; while(i<T[0]) //此处T[0]表示串T的长度 { //T[i]表示后缀的单个字符 //T[j]表示前缀的某个单词 if(j == 0 || T[i] == T[j]) { ++i; ++j; next[i] = j; } else { j = next[j]; //若字符不相同,则j值回溯 } } }
- 返回子串T在主串S中第pos个字符串之后的位置。若不存在,则返回0
T非空,1 <= pos <=StrLength(S)int Index_KMP(String S, String T, int pos) { int i = pos; //i用于主串S当前位置下标值,若pos不为1,则从pos位置开始匹配 int j = 1; //j用于子串T中当前位置下标值 int next[255]; //定义next数组 get_next(T, next); //对串T作分析,得到next数组 while(i <= S[0] && j <= T[0])//若i小于S长度且j小于T长度,循环继续 { //两字母相等则继续,与朴素算法相比增加了j=0判断 if(j == 0 || S[i] == T[j]) { ++i; ++j; } else //子串指针后退重新开始匹配 { j = next[j]; //j退回到合适的位置,i值不变 } } if(j > T[0]) return i-T[0]; else return 0; }
KMP模式匹配算法改进—对于子串T中next函数的修正
- 通过计算返回子串T的next数组
void get_next(String T, int *next) { int i, j; i = 1; j = 0; next[1] = 0; while(i<T[0]) //此处T[0]表示串T的长度 { //T[i]表示后缀的单个字符 //T[j]表示前缀的某个单词 if(j == 0 || T[i] == T[j]) { ++i; ++j; if(T[i] != T[j]) //当前字段与前字段不同 next[i] = j; else //若前缀字符相同则将前缀字符的next值赋给当前next值 next[i] = next[j]; } else { j = next[j]; //若字符不相同,则j值回溯 } } }
5、树
定义:树(Tree)是n(n>=0)个结点的有限集。n=0时称为空树。在任意一颗非空树中;(1)有且仅有一个特定的称为根(Root)的结点;(2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、……、Tm,其中每一个集合本身又是一颗树,并且称为根的子树(SubTree),如图6-2-1所示:
5.1、二叉树性质
性质1:在二叉树的第i层上至多有2i-1个结点(i>=1)
性质2:深度为k的二叉树至多有2k-1个结点(k>=1)
性质3:对任何一颗二叉树T,如果其终端结点树为n0,度为2的结点数为n2,则 n0 = n2+1
性质4:具有n个结点的完全二叉树的深度为|log2n|(表示不超过log2n最大整数)+1
性质5:如果对一颗有n个结点的完全二叉树(其深度为|log2n|+1)的结点按层序编号(从第1层到第|log2n|+1层,每层从左到右),对任一结点i(1<= i <=n)有:
- 如果i=1,则结点i是二叉树的跟,无双亲;如果i>1,则其双亲是结点|i/2|
- 如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i
- 如果2i+1>n,则结点i无有孩子;否则其右孩子是结点2i+1
5.2、二叉树的存储
顺序存储结构一般只用于完全二叉树
二叉链表:二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域,我们称这样的链表叫做二叉链表
- 二叉树的二叉链表结点结构定义
typedef struct BiTNode { TElemType data; //结点数据 struct BiTNode *lchild, *rchild; //左右孩子指针 }BiTNode, *BiTree;
结构示意图如图所示:
5.3、遍历二叉树
定义:二叉树的遍历是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次
如图所示:
前序遍历:ABDGHCEIF
中序遍历:GDHBAEICF
后序遍历:GHDBIEFCA
层序遍历:ABCDEFGHI
- 二叉树的前序遍历递归算法
void PreOrderTraverse(BiTree T) { if(T == NULL) return; printf("%c", T->data); //显示结点数据 PreOrderTraverse(T->lchild); //先遍历左子树 PreOrderTraverse(T->rchild); //最后遍历右子树 }
- 二叉树的中序遍历递归算法
void PreOrderTraverse(BiTree T) { if(T == NULL) return; PreOrderTraverse(T->lchild); //先遍历左子树 printf("%c", T->data); //显示结点数据 PreOrderTraverse(T->rchild); //最后遍历右子树 }
- 二叉树的后序遍历递归算法
void PreOrderTraverse(BiTree T) { if(T == NULL) return; PreOrderTraverse(T->lchild); //先遍历左子树 PreOrderTraverse(T->rchild); //再遍历右子树 printf("%c", T->data); //显示结点数据 }
- 二叉树的建立
按前序输入二叉树中结点的值(一个字符)
#表示空树,构造二叉链表表示二叉树Tvoid CreateBiTree(BiTree *T) { TElemType ch; scanf("%c", &ch); if(ch == '#') *T = NULL; else { *T = (BiTree)malloc(sizeof(BiTNode)); if(!*T) exit(OVERFLOW); (*T)->data = ch; //生成根结点 CreateBiTree(&(*T)->lchild); //构造左子树 CreateBiTree(&(*T)->rchild); //构造右子树 } }
线索二叉树:有这种指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树
lchild-ltag-data-rtag-rchild
ltag为0时指向该结点的左孩子,为1时指向该结点的前驱
rtag为0时指向该结点的右孩子,为1时指向该结点的后继
- 线索二叉树结构实现
typedef enum {Link, Thread} PointerTag;//Link == 0表示指向左右孩子指针 //Thread == 1表示指向前驱或后继的线索 typedef struct BiThrNode //二叉线索存储结点结构 { TElemType data; //结点数据 struct BiThrNode *lchild, *rchild; //左右孩子指针 PointerTag LTag; PointerTag RTag; //左右标志 }BiThrNode, *BiThrTree;
- 中序遍历线索化的递归函数代码如下
BiThrTree pre; //全局变量,始终指向刚刚访问过的结点 void InThreading(BiThrTree p) { if(p) { InThreading(p->lchild); //递归左子树线索化 if(!p->lchild) //没有左孩子 { p->LTag = Thread; //前驱线索 p->lchild = pre; //左孩子指针指向前驱 } if(!pre->rchild) //前驱没有右孩子 { pre->RTag = Thread; //后继线索 pre->rchild = p; //前驱右孩子指针指向后继(当前结点P) } pre = p; //保持pre指向p的前驱 InThreading(p->rchild); //递归右子树线索化 } }
5.4、赫夫曼树及其应用
定义:赫夫曼编码是美国数学家赫夫曼发明的,他的编码用到了特殊二叉树,我们称之为赫夫曼树
我们同时将带权路径长度WPL最小的二叉树称为赫夫曼树,我们总是将最小的两个值组成一个二叉树,父级值为两个孩子之和,然后将这个二叉树顶点再放入集合对象,从新选择最小的两个值,组成二叉树,以此类推生成最优二叉树
应用:用于压缩文件、解决数据传输最优化问题
6、图
定义:图是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合
图的数据结构
- ADT图(Graph)
- Data
顶点的有穷非空集合和边的集合 - Operation
GreateGraph(*G, V, VR):按照顶点集V和边弧集VR的定义构造图G DestroyGraph(*G):图G存在则销毁 LocateVex(G,u):若图G中存在顶点u,则返回图中的位置 GetVex(G,v):返回图G中顶点v的值 PutVex(G,v,value):将图G中顶点v赋值value FirstAdjVex(G,*v):返回顶点v的一个邻接顶点,若顶点在G中无邻接顶点返回空 NextAdjVex(G,v,*w):返回顶点v相对于顶点w的下一个邻接顶点,若w是v的 最后一个邻接点则返回“空” InsertVex(*G,v):在图G中增添新顶点v DeleteVex(*G,v):删除图G中顶点v及其相关的弧 InsertArc(*G,v,w):在图G中增添弧<v,w>,若G是无向图,还需要增添 对称弧<w,v> DeleteArc(*G,v,w):对图G中删除弧<v,w>,若G是无向图,则还删除对称弧<v,w> DFSTraverse(G):对图G中进行深度优先遍历,在遍历过程对每个顶点调用 HFSTraverse(G):对图G中进行广度优先遍历,在遍历过程对每个顶点调用
- endADT
6.1、图的存储
6.1.1、邻接矩阵
图的邻接矩阵存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息
- 1.1我们要判定任意两顶点是否有边无边就非常容易了
- 1.2我们要知道某个顶点的度,其实就是这个顶点vi在邻接矩阵中第i行(或第i列)的元素之和。比如顶点vi的度就是1+0+1+0=2
- 1.3求顶点vi的所有邻接点就是将矩阵中第i行元素扫描一遍,arc[i][j]为1就是邻接点
- 邻接矩阵存储结构
typedef char VertexType; //顶点类型应由用户定义 typedef int EdgeType; //边上的权值类型应由用户定义 #define MAXVEX 100 //最大顶点数,由用户定义 #define INFINITY 65535 //用65535来代表无限 typedef struct { VertexType vexs[MAXVEX]; //顶点表 EdgeType arc[MAXVEX][MAXVEX]; //邻接矩阵,可看作边表 int numVertexes, numEdges; //图中当前的顶点数和边数 }MGraph;
- 无向网-构造图
void CreateMGraph(MGraph *G) { int i, j, k, w; printf("输入顶点数和边数:\n"); scanf("%d, %d", &G->numVertexes, &G->numEdges); //输入顶点数和边数 for(i = 0; i < G->numVertexes; i++) //读入顶点信息,建立顶点表 scanf(&G->vexs[i]); for(i = 0; i < G->numVertexes; i++) for(j = 0; j < G->numVertexes; j++) G->arc[i][j] = INFINITY; //初始化邻接矩阵 for(k = 0; k < G->numEdges; k++) //读入numEdges条边,建立邻接矩阵 { printf("输入边(vi,vj)上的下标i,下标j和权w:\n"); scanf("%d, %d, %d", &i, &j, &w); //输入边(vi,vj)上的权w G->arc[i][j] = w G->arc[j][i] = G->arc[i][j]; //因为是无向图,矩阵对称 } }
6.1.2、邻接表
定义:数组与链表相结合的存储方法称为邻接表
无向图邻接表结构如下:
有向图邻接表如下:
- 邻接表存储结构
typedef char VertexType; //顶点类型应由用户定义 typedef int EdgeType; //边上的权值类型应由用户定义 typedef struct EdgeNode //边表结点 { int adjvex; //邻接点域,存储该顶点对应的下标 EdgeType weight; //用于存储权值,对于非网图可以不需要 struct EdgeNode *next; //链域,指向下一个邻接点 }EdgeNode; typedef struct VertexNode //顶点表结点 { VertexType data; //顶点域,存储顶点信息 EdgeNode *firstedge; //边表头指针 }VertexNode, AdjList[MAXVEX]; typedef struct { AdjList adjList; int numVertexes, numEdges; //图中当前顶点数和边数 }GraphAdjList;
- 邻接表的无向图创建
void CreateMGraph(GraphAdjList *G) { int i, j, k; EdgeNode *e; printf("输入顶点数和边数:\n"); scanf("%d,%d", &G->numVertexes, &G->numEdges); //输入顶点数和边数 for(i = 0; i < G->numVertexes; i++) //读入顶点信息,建立顶点表 { scanf(&G->adjList[i].data); //输入顶点信息 G->adjList[i].firstedge = NULL; //将边表置为空表 } for(k = 0; k < G->numEdges; k++) //建立边表 { printf("输入边(vi,vj)上的顶点序号:\n"); scanf("%d, %d", &i, &j); //输入边(vi,vj)上的顶点序号 e = (EdgeNode *)malloc(sizeof(EdgeNode));//向内存申请空间 //生成边表结点 e->adjvex = j; //邻接序号为j e->next = G->adjList[i].firstedge; //将e指针指向当前顶点指向的结点 G->adjList[i].firstedge = e; //将当前顶点的指针指向e e->adjvex = i; //邻接序号为i e->next = G->adjList[j].firstedge; //将e指针指向当前顶点指向的结点 G->adjList[j].firstedge = e; //将当前顶点的指针指向e } }
6.1.3、边集数组
边集数组是由两个一维数组构成。一个是存储顶点的信息;另一个是存储边的信息,这个边数组每个数据元素由一条边的起点下标、终点下标和权组成
6.2、图的遍历
定义:从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次,这一过程就叫做图的遍历
6.2.1、深度优先遍历
深度优先遍历,也有称为深度优先搜索,简称为DFS
- 邻接矩阵的深度优先递归算法
typedef int Boolean; //Boolean是布尔类型,其值是TRUE或FALSE Boolean visited[MAX]; //访问标志的数组 void DFS(MGraph G, int i) { int j; visited[i] = TRUE; printf("%c ", G.vexs[i]); //打印顶点,也可以其他操作 for(j = 0; j < G.numVertexes; j++) if(G.arc[i][j] == 1 && !visited[j]) DFS(G, j); //对为访问的邻接顶点递归调用 }
- 临接矩阵的深度遍历操作
void DFSTraverse(MGraph G) { int i; for(i = 0; i < G.numVertexes; i++) visited[i] = FALSE; //初始所有顶点状态都是未访问过状态 for(i = 0; i < G.numVertexes; i++) if(!visited[i]) //对未访问过的顶点调用DFS,若是连通图,只会执行一次 DFS(G, i); }
- 递归函数中因为将数组换成了链表而有不同,代码如下
void DFS(GraphAdjList GL, int i) { EdgeNode *p; visited[i] = TRUE; printf("%c ", GL->adjList[i].data); //打印顶点,也可以其他操作 p = GL->adjList[i].firstedge; while(p) { if(!visited[p->adjvex]) DFS(GL, p->adjvex); //对为访问的邻接顶点递归调用 p = p->next; } }
- 邻接表的深度遍历操作
void DFSTraverse(GraphAdjList GL) { int i; for(i = 0; i < GL->numVertexes; i++) visited[i] = FALSE; //初始所有顶点状态都是未访问状态 for(i = 0; i < GL->numVertexes; i++) if(!visited[i]) //对未访问过的顶点调用DFS,若是连通图,只会执行一次 DFS(GL, i); }
6.2.2、广度优先遍历
广度优先遍历,又称为广度优先搜索,简称DFS
- 邻接矩阵的广度遍历算法
void BFSTraverse(MGraph G) { int i, j; Queue Q; for(i = 0; i < G.numVertexes; i++) visited[i] = FALSE; InitQueue(&Q); //初始化-辅助用的队列 for(i = 0; i < G.numVertexes; i++) //对每一个顶点做循环 { if(!visited[i]) //若是本访问过就处理 { visited[i] = TRUE; //设置当前顶点访问过 printf("%c ", G.vexs[i]); //打印顶点,也可以其他操作 EnQueue(&Q, i); //将此顶点入队列 while(!QueueEmpty(Q)) //若当前队列不为空 { DeQueue(&Q, &i); //将队中元素出队列,赋值给i for(j = 0; j < G.numVertexes; j++) { //判断其他顶点若与当前顶点存在边且未访问过 if(G.arc[i][j] == 1 && !visited[j]) { visited[j] = TRUE; //将找到的此顶点标记为已访问 printf("%c ", G.vexs[j]); //打印顶点 EnQueue(&Q, j); //将找到的此顶点入队列 } } } } } }
6.3、最小生成树
定义:把构造连通网的最小代价生成树称为最小生成树
- Prim算法生成最小生成树
void MiniSpanTree_Prim(MGraph G) { int min, i, j, k; int adjvex[MAXVEX]; //保存相关顶点下标 int lowcost[MAXVEX]; //保存相关顶点间边的权值 lowcost[0] = 0; //初始化第一个权值为0,即v0加入生成树 //lowcost的值0,在这里就是此下标的顶点已经加入生成树 adjvex[0] = 0; //初始化第一个顶点下标为0 for(i = 1; i < G.numVertexes; i++) //循环除下标0外的全部顶点 { lowcost[i] = G.arc[0][i]; //将v0顶点与之有边的权值存入数组 adjvex[i] = 0; //初始化都为v0的下标 } for(i = 1; i < G.numVertexes; i++) { min = INFINITY; //初始化最小权值为无限,如32767/65535 j = 1; k = 0; while(j < G.numVertexes) //循环全部顶点 { if(lowcost[j] != 0 && lowcost[j] < min) { min = lowcost[j] //则让当前权值成为最小值 k = j; //将当前最小值的下标存入k } j++; } printf("(%d, %d)", adjvex[k], k); //打印当前顶点边中权值最小边 lowcost[k] = 0; //将当前顶点的权值设置为0,表示此顶点已经完成任务 for(j = i; j < G.numVertexes; j++) //循环所有顶点 { if(lowcost[j] != 0 && G.arc[k][j] < lowcost[j]) { lowcost[j] = G.arc[k][j]; //将较小权值存入lowcost adjvex[j] = k; //将下标为k的顶点存入adjvex } } } }
- Kruskal算法生成最小生成树
void MiniSpanTree_Kruskal(MGraph G) //生成最小生成树 { int i, n, m; Edge edges[MAXEDGE]; //定义边集数组 int parent[MAXVEX]; //定义一数组用来判断边与边是否形成环路 //此处省略将邻接矩阵G转化为边集数组edges并按权由小到大排序的代码 for(i = 0; i < G.numVertexes; i++) parent[i] = 0; //初始化数组值为0 for(i = 0; i < G.numEdges; i++) //循环每一条边 { n = Find(parent, edges[i].begin); m = Find(parent, edges[i].end); if(n != m) //假如n与m不等,说明此边没有与现有生成树形成环路 { parent[n] = m; //将此边的结尾顶点放入下标为起点的parent中 //表示此顶点已经在生成树集合中 printf("(%d, %d) %d", edges[i].begin),edges[i].end, edges[i].weight); } } } int Find(int *parent, int f) //查找连线顶点的尾部下标 { while(parent[f] > 0) f = parent[f]; return f; }
6.4、最短路径
定义:对于网图来说,最短路径,是指两顶点之间经过的边上权值之和最少的路径,并且我们称路径上的第一个顶点是源点,最后一个顶点是终点
- 最短路径
#define MAXVEX 9 #define INFINITY 65535 typedef int Pathmatirx[MAXVEX]; //用于存储最短路径下标的数组 typedef int ShortPathTable[MAXVEX]; //用于存储到各点最短路径的权值和 //迪杰斯特拉(Dijkstra)算法 //求有向网G的v0顶点到其余顶点v最短路径p[v]及带权长度D[v] //p[v]的值为前驱顶点下标,D[v]表示v0到v的最短路径长度和 void ShortestPath_Dijkstra(MGraph G, int v0, Pathmatirx *p, ShortPathTable *D) { int v, w, k, min; int final[MAXVEX]; //final[w] = 1表示求得顶点v0至vw的最短路径 for(v = 0; v < G.numVertexes; v++) //初始化数据 { final[v] = 0; //全部顶点初始化为未知最短路径状态 (*D)[v] = G.matirx[v0][v]; //将与v0点有连线的顶点加上权值 (*P)[v] = 0; //初始化路径数组P为0 } (*D)[v0] = 0; //v0至v0路径为0 final[v0] = 1; //vo至v0不需要求路径 //开始主循环,每次求得v0到某个v顶点的最短路径 for(v = 1; v < G.numVertexes; v++) { min = INFINITY; //当前所知离v0顶点的最近距离 for(w = 0; w < G.numVertexes; w++) //寻找离v0最近的顶点 { if(!final[w] && (*D)[w] < min) { k = w; min = (*D)[w]; //w顶点离v0顶点更近 } } final[k] = 1; //将目前找到的最近的顶点位置为1 for(w = 0; w < G.numVertexes; w++) { //如果经过v顶点的路径比现在这条路径的长度 if(!final[w] && (min+G.matirx[k][w] < (*D)[w])) { (*D)[w] = min + G.matirx[k][w]; //修改当前路径长度 (*p)[w] = k; } } } }
小伙初来乍到,多有不懂,如有错误欢迎指出