计算时间复杂度
输入规模-操作数量f(n)
int count=1;
while(count<n)
{
count=cout*2;
}
输入规模n,要操作多少次能够=n,有多少个2(乘多少次2)相乘就能够大于n,2x=n, x=log2n,也就是需要操作log2n次
O(1)
2) 3) n) n)
一般提到的运行时间都是最坏情况下的运行时间
平均时间复杂度:计算所有情况的平均值
算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作s(n)=O(f(n)),n为问题的规模,f(n)为语句关于n所占存储空间的函数——没懂
数组点的长度不等于线性表的长度,数组长度是存放线性表的存储空间的长度,分配后不变,线性表长度是线性表中数据元素的个数,可以变化
线性表的长度应该小于等于数组的长度
线性表的顺序存储结构,在存、读数据时,复杂度为O(1)。在删除和插入时,复杂度为O(n)。
头指针:链表中第一个节点的存储位置叫做头指针。头指针是链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针。头指针不能为空,长以链表的名字命名。是链表的必要元素
头结点的指针域存储指向第一个结点的指针。头结点不是必要元素
注意头结点和头指针的区别
q=p->next;
p->next=q->next;
free(q);
p=rearA-_next;
rearA->next=rearB->next->next;
rearB->next=p;
free(p);
typedef struct DulNode
{
ElemType data;
struct DulNode* prior;
struct DulNode* next;
}DulNode, *DuLinkList;//可以把DuLinkList等价于 struct DulNode*
插入元素-记忆
s->prior=p;
s->next=p->next;
p->next->prior=s;
p->next=s;
删除元素-记忆
p->prior->next=p->next;
p->next->prior=p->prior;
free(p);
栈是限定仅在表尾进行插入和删除操作的线性表
队列是只允许在一端进行插入操作、而在另一端进行删除操作的线性表
栈顶:允许插入和删除的一端,另一端叫栈底
空栈:不含任何数据元素
栈:后进先出的线性表,LIFO结构
通常把空栈的判断条件定为top=-1
typedef int SElemType;
typedef struct
{
SElemType data[MAXSIZE];
int top;
}SqStack;
栈的顺序存储结构必须事先知道数组存储空间的大小
用一个数组存储两个栈,一个栈底为数组首元素,一个栈底为数组末端,增加栈元素则是想数组中间延伸
typedef struct
{
SElemType data[MAXSIZE];
int top1;
int top2;
}SqDoubleStack;
把栈顶放在单链表的头部,对于栈链来说,不需要头结点
typedef struct StackNode
{
SElemType data;
struct StackNode* next;
}StackNode,*LinkStackPtr;
typedef struct LinkStack
{
LinkStackPtr top;//相当于struct StackNode* top
int count;
}LinkStack
status Push(LinkStack* S,SElemType e)
{
LinkStackPtr s=(LinkStackPtr)malloc(sizeof(StackNode));
//相当于struct StackNode* s=(struct StackNode*)malloc(StackNode);
s->data=e;
s->next=S->top;//把当前的栈顶元素赋值给新节点的直接后继
//二者都是struct StackNode*类型
S->top=s;//将新的节点s赋值给栈顶指针
S->count++;
return OK:
}
status Push(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);
S->count--;
return OK:
}
链栈的进出的时间复杂度为O(1)
函数调用函数自己,可以看成调用一个与自己长得一样的另一个函数
在前行阶段,对于每一层递归,函数的局部变量、参数值以及返回地址都被压入栈中,在退回阶段,位于栈顶的局部变量、参数值和返回地址被弹出,用于返回调用层次中执行代码的其余部分
先进先出的功能。队列是只允许在一段进行插入操作,而在另一端进行删除操作的线性表
允许插入的一端为队尾,允许删除的一端为队头。
front指针指向队头元素,rear元素指针指向队尾元素的下一个位置,当front=rear时,此队列不是还剩一个元素,而是空队列
初始状态,front和rear均指向下标为0的位置,然后入队
循环队列解决假溢出的问题
//结构
typedef int QElemType;
typedef struct
{
QElemType data[MAXSIZE];
int front;//头指针
int rear;//尾指针,若队列不空,指向队列尾元素的下一个位置
}SqQueue;
//循环队列初始化
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;
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];
Q->front=(Q->front+1)%MAXSIZE;//front指针向后移一位置,若到最后则转到数组头部
}
队列的链式存储结构,就是线性表的单链表,只不过只能尾进头出,称之为链队列
令队头指针指向链队列的头结点,队尾指针指向终端结点(不是终端结点的下一个,注意)
空队列时,front和rear都指向头结点
typedef int QElemType;
typedef struct QNode;
{
QElemType data;
struct QNode* next;
}QNode,*QueuePtr;
typedef struct
{
QueuePtr fonrt,rear;//相当于struct QNode* 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;
}
Status EnQueue(LinkQueue* Q, QElemType* e)
{
QueuePtr p;
if(Q->front==Q->rear)
return ERROR;
p=Q->front->next;
*e=p->data;
Q->front->next=p->next;
if(Q->rear==p)
Q->rear=Q->front;
free(p);
return OK;
}
子串的定位称为串的模式匹配
//主串长度和子串长度存放在S[0]和T[0]中
int Index(String S,String T,int pos)
{
int i=pos;
int j=1;
while(i<=S[0]&&j<=T[0])
{
if(S[i]==T[j])
{
i++;
j++;
}
else
{
i=i-j+2;
j=1;
}
}
if(j>T[0])
return i-T[0];
else
return 0;
}
核心就是
//通过计算返回子串中的next数组
void get_next(String T,int *next)
{
int i,j;
i=1;//数组从1开始
j=0;
next[1]=0;//next[1]一般都是0
while(i<T[0])//T[0]记录子串的长度
{
if(j==0||T[i]==T[j])//T[i]表示后缀的单个字符;T[j]表示前缀的单个字符
{
i++;
j++;
next[i]=j;
}
else
{
j=next[j];
}
}
}
度:结点拥有的子树数
度为0的结点称为叶结点
树的度为树内各结点的度的最大值
孩子,双亲,兄弟
结点的祖先是从根到该结点所经分支上的所有结点
注意:该结点的子树的根,不是该结点,是该结点的孩子
层次:树的层次从根开始,根为第一层,以此类推(结点在l层,该结点的子树的根在l+1层)
深度:树中结点的最大层次叫深度(高度)
将树中结点的各子树看成从左至右是有次序的,不能互换的,则称为有序树,反之为无序树
森林:m棵互不相交的树的集合。对树中的每个结点而言,其子树的集合即为森林
设一组连续空间存储树的结点,同时每个结点中,附设一个指示器指示双亲结点到链表中的位置[data,parent],data存储结点的数据信息,parent指针域,存储该结点的双亲在数组中的下标
#define MAX_TREE_SIZE 100
typedef int TElemType;
typedef struct PTNode
{
TElemType data;//结点数据
int parent;//双亲位置
}PTNode;
typedef struct
{
PTNode nodes[MAX_TREE_SIZE];//结点数组
int r,n;//根的位置和结点数
}PTree;
树中的每个结点可能有多棵子树,考虑用多重链表,每个结点有多个指针域,其中每个指针域指向一颗子树的根结点,该方法为多重量表表示法。但是树的每个结点的度不同
方案一:
指针域的个数等于树的度
data,child1,...,childN
缺点:当树中的各结点的度相差很大时,会浪费空间,很多结点的指针域是空的
方案二:
每个结点指针域的个数等于该结点的度,专门用一个位置存储该结点指针域的个数
data,degree,child1,...,childM
缺点:维护起来麻烦
孩子表示法
把每个结点的孩子结点排列起来,以单链表作为存储结构,n个几点有n个孩子链表,如果是叶子结点单链表为空。然后n个头指针又自成一格线性表,采用顺序存储结构,放进一个一维数组中
两种结点结构:
孩子链表结构
child,next
child为数据域,用于存储某个结点在表头数组中的下标,就是该结点的第一个孩子结点所在表头数组中的位置
next为指针域,用来存储指向某结点的下一个孩子结点的指针,就是该结点(表头结点)的下一个孩子,即第二个、第三个孩子,就是第一个孩子的兄弟
表头数组的表头结点
data,firstchild
firstchild存储该结点的孩子链表的头指针
//孩子表示法
#define MAX+TREE_SIZE 100
//孩子结点
typedef struct CTNode
{
int child;
struct CTNode* next;
}*ChildPtr;
//表头结构
typedef struct
{
TElemType data;
ChildPtr firstchild;//包含了孩子链表的头指针
}CTBox;
typedef struct
{
CTBox nodes{MAX_TREE_SIZE};//结点数组
int r,n;//根的位置和结点数
}
任意一棵树,结点的第一个孩子如果存在就是唯一的,他的右兄弟如果存在也是唯一的。设置两个结点指向该结点的第一个孩子和此结点的右兄弟
data,firstchild,rightsib
typedef struct CSNode
{
TElemType data;
struct CSNode* firstchild,*rightsib;
}CSNode,*CSTree;
图P162
每个根结点只有两个子树,左子树和右子树,二叉树的度最大为2
左右子树是由顺序的,次序不能颠倒。即使某结点只有一颗子树,也要区分是左子树和右子树
二叉树的五种形态
空二叉树
只有一个根结点
根结点只有左子树
根结点只有右子树
根结点有左右子树
所有结点都只有左子树为左斜树
所有结点都只有右子树为右斜树
即每一层只有一个结点,结点的个数与二叉树的深度相同
所有分支结点都有左右子树,且所有的叶子都在同一层上
叶子只出现在最下一层
非叶子的结点的度都是2
同样深度的二叉树中,满二叉树的结点个数最多
当等于就是按层序编号,与满二叉树层序编号的结果一致,但是叶子可能不全在最下一层
叶子结点只能出现在最下两层
最下层的叶子一定集中在左部连续位置
倒数二层,若有叶子结点,一定都在右部连续位置
如果结点度为1,则该结点只有左孩子,一定没有右孩子
同样结点数的二叉树,完全二叉树的深度最小
二叉树的第i层上至多有2i-1个结点
深度为k的二叉树最多有2k-1个几点
任何一棵树,终端结点数(叶子结点数)为 n 0 n_0 n0,度为2的结点数为 n 2 n_2 n2,则 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1
推导:
n 1 n_1 n1为度为1的结点数, n 1 2 n_12 n12为度为2的结点数,总结点数为 n n n,总分支数为 = n − 1 = n 1 + 2 n 2 ( 没 懂 ) =n-1=n_1+2n_2(没懂) =n−1=n1+2n2(没懂)
n 0 + n 1 + n 2 − 1 = n 1 + 2 n 2 : − > n 0 = n 2 + 1 n_0+n_1+n_2-1=n_1+2n_2 :->n_0=n_2+1 n0+n1+n2−1=n1+2n2:−>n0=n2+1
n个结点的完全二叉树的深度为 ( l o g 2 n ) + 1 (log_2n)+1 (log2n)+1,表示不大于 ( l o g 2 n ) (log_2n) (log2n)的整数,然后再+1
对于满二叉树,深度为k,结点数为n= 2 k − 1 2^k-1 2k−1,k= l o g 2 ( n + 1 ) log_2(n+1) log2(n+1)
利用顺序存储结构来定义二叉树,最简单的是定义完全二叉树,数组的对应下标能够反映结点之间的逻辑关系。对于不是完全二叉树的任意形式二叉树,可以按照完全二叉树进行存储,不存在的结点在数组中设置为空。这样会浪费空间,因此一般顺序存储结构只用于完全二叉树
每个结点最多有两个孩子,可以设置两个指针
lchild,data,rchild
结点结构
typedef struct BiTNode
{
TElemType data;
struct BiNode* lchild,*rchild;
}BiTNode,*BiTree;
void PreOrderTraverse(BiTree T)
{
if(T==NUll)
return;
printf("%c",T->data);//按字符输出
PreOrderTraverse(T->lchild);//左孩子,lchild和rchild和T都是struct BiTNode*结构的
PreOrderTraverse(T->rchild);//右孩子
}
注意:当函数执行完毕的时候,自动返回到上一级递归的函数中去
具体递归图解见书P180
void InOrderTraverse(BiTree T)
{
if(T==NUll)
return;
InOrderTraverse(T->lchild);//左孩子,lchild和rchild和T都是struct BiTNode*结构的
printf("%c",T->data);//按字符输出
InOrderTraverse(T->rchild);//右孩子
}
具体递归图解见书P183
void PostOrderTraverse(BiTree T)
{
if(T==NUll)
return;
PostOrderTraverse(T->lchild);//左孩子,lchild和rchild和T都是struct BiTNode*结构的
PostOrderTraverse(T->rchild);//右孩子
printf("%c",T->data);//按字符输出
}
已知前序和中序,得唯一二叉树
已知后序和中序,得唯一二叉树
已知前序和后序,不能确定二叉树
扩展二叉树:将每个结点的空指针引出一个虚结点,其值为一个特定值,如#
//假设二叉树的结点均为一个字符
//按照前序输入二叉树中结点的值(一个字符)
//#表示空树,构造二叉链表表示二叉树$T_0$
void CreateBiTree(BiTree* T)
{
TElemType ch;//字符型
scanf("%c",&ch);//输入字符
if(ch=='#')
*T=NUll;
else
{
*T=(BiTree)malloc(sizeof(BiTNode));//开辟一个结点,BiTree为struct BiTNode*
if(!*T)
exit(OVERFLOW);
(*T)->data=ch;//生成根结点
CreateBiTree(&(*T)->lchild);//构建左子树
CreateBiTree(&(*T)->rchild);//构建右子树
}
}
加上线索的二叉链表为线索链表,加上线索的二叉树为线索二叉树
线索化的实质就是将二叉链表中的空指针改为指向前驱或后去的线索
由于前驱和后继的信息只有在遍历二叉树时才能得到,所以线索化的过程就是在遍历的过程中修改空指针的过程
typedef enum{Link,Thread}PointerTag;//enum声明了一个枚举类型,link==0表示指向左右孩子指针,Thread==1表示指向前驱后继指针
/*
这个相当于结构体
typedef enum
{
Link,
Thread,
}PointerTag;
*/
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);//递归左子树线索化
//###
}
}
利用孩子兄弟表示法,进行转换
加线。在所有兄弟之间加一条线
去线。对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线
层次调整。以树的根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明。注意第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是节点的右孩子。
实例见图
森林中的每一棵树都是兄弟,可以按照兄弟的处理办法来操作
把每棵树都转化为二叉树
第一棵二叉树不动,从第二棵二叉树开始,依次把后一颗二叉树的根结点作为前一棵二叉树的根结点右孩子,用线连接起来,当所有的二叉树连接起来后就得了由森林转化来的二叉树
加线。若某结点的左孩子结点存在,则将这个左孩子的右结点、右孩子的右孩子结点,即左孩子的n个右孩子结点都作为都作为此结点的孩子,将该结点与这些右孩子结点用线连接起来
去线。删除原二叉树中所有结点与其右孩子结点的连线
层次调整。使之结构层次分明
判断二叉树能否转化为树或森林。只要看这棵二叉树的根结点有没有右孩子,有就是森林,没有就是一棵树。
从根结点开始,若右孩子存在,则把与右孩子结点的连线删除,再查看分离后的二叉树,若右孩子存在,则连线删除。直到所有右孩子连线都删除为止,得到分离的二叉树。
再将每棵分离后的二叉树转换为树即可
树的遍历:
先根遍历。即先访问树的根结点,即先访问根。然后再访问根的没棵子树
后根遍历。先访问根的子树,再访问根
森林的遍历:
前序遍历。先访问森林中的第一棵树的根结点,然后再依次先根遍历根的没棵子树。然后遍历第二棵树
后序遍历。先访问森林中的第一棵树,再按后根遍历方法
基本的压缩编码法
结点的带权的路径长度,为从该结点到树根之间的路径长度与结点上权的乘积
树的带权的路径长度,为树中所有叶子结点的带权路径长度之和
赫夫曼树(最优二叉树):带权路径长度WPL最小的二叉树
赫夫曼树的构造:书P204
赫夫曼算法:
构造赫夫曼树,并将权值做分支变为0,将权值右分支变为1。
然后对每个叶子结点利用路径经过的01进行编码
利用赫夫曼树进行解码——没细讲,没懂