目录
第一章-绪论
第二章-线性表
3.顺序表表示
4.顺序表基本运算
5.链表
6.链表的基本运算
7.循环链表
8.双链表
9.静态链表
10.一元多项式表示及相加
第三章-限定性线性表(栈与队列)
1.顺序栈
2.链栈
3.链队列
4.循环队列
5.习题
第四章-串
1.定长顺序串
2.堆串
3.块链串
第五章-数组和广义表
1.数组地址计算
2.特殊矩阵的压缩存储
3.广义表
第六章-树与二叉树
1.树
2.二叉树
3.二叉树链式存储结构
4.二叉树的遍历
5.线索二叉树
6.树,森林,二叉树的关系
7.哈夫曼树
8.习题
第七章-图
第一章-绪论
1.数据元素是组成数据的基本单位,是数据集合的个体。
2.数据项是有独立含义的最小单位,此时的数据元素通常称为记录。
3.数据对象是性质相同的数据元素的集合,是数据的一个子集。
4.数据结构是指相互之间存在一种或多种特定关系的数据元素集合。
5.抽象数据类型最重要的特点是数据抽象与信息隐蔽。
6.数据元素之间的相互关系具体应包括三个方面:数据的逻辑结构,数据的存储结构,数据的运算集合。
7.根据数据元素之间的不同特性,通常有下列四类基本结构:集合结构(无任何关系),线性结构(一对一),树状结构(一对多),图状结构(多对多)。
8.算法的特性:有限性,确定性,可行性,输入,输出。
9.算法设计的要求:正确性,可读性,鲁棒性,高效率和低存储量。
10.语句频度:语句在一个算法中重复执行的次数。
E.G:
int i = 1;
int k = 0;
int n = 10;
while(i <= n-1){
k += 10 * i; /*语句频度:n-1*/
i++;
}
int i = 1;
int k = 0;
int n = 10;
do{
k += 10 * i; /*语句频度:n-1*/
i++;
}while(i <= n-1);
int k = 0;
int n = 10;
for(int i = 1; i <= n; i++){
for(int j = i; j<=n; j++){
k++; /*第一层循环 n 次
第二层循环 n+(n-1)+(n-2)+…+2+1 = n(n+1)/2 ,所以 k++ 执行了 n(n+1)/2 次*/
}
}
ps:一個等差數列的和,等於其首項與末項的和,乘以項數除以2。
int n = 100;
int i = 0;
while(n >= (i+1)*(i+1)){
i++; /*语句频度:sqrt(n)*/
}
int x = 0;
int i,j,k;
int n = 10;
for(i = 1; i <= n; i++) {
for(j = 1; j <= i ; j++) {
for(k = 1; k <= j; k++) {
x += 1; /*语句频度:n(n+1)(n+2)/6*/
}
}
}
11.时间复杂度是由嵌套层数最多的循环语句中最内层语句的频度决定的
E.G:
//O(n)
for(int i=0;i<n;i++){
System.out.println(result[i]);
}
//O(logn)
int result=1;
while(result<n){
result=result*2;
}
12.空间复杂度是该算法所耗费的存储空间的数量级
E.G:若算法执行时所需要的辅助空间是一个常数,则空间复杂度为O(1);
若需要一个一维数组,空间复杂度为O(n);
若需要一个二维数组,空间复杂度为O(n^2);
13.变量的作用域是指:包含改变量定义的最小范围。
14.一种抽象类型包括:数据对象,对象间关系,一组处理数据的操作。
15.数据结构的存储结构分为:顺序,非顺序两种
16.结构化程序设计的方法:①自顶向下,逐步求精;
②独立功能,一个入口,一个出口;
③仅用三种基本控制结构(顺序,选择,重复)。
17.函数结果三种带出方式:全程变量,函数返回值,传址参数。
第二章-线性表
1.线性表的特点:同一性(每个ai必须属于同一数据类型),有穷性(表长度就是表中元素个数),有序性(相邻元素间存在<ai,ai+1>序偶关系)。
2.顺序表可以实现随机存取。
3.顺序表表示
#define MAXSIZE 100
typedef struct{
ElemType elem[MAXSIZE];
int last;
}SeqList;
4.顺序表基本运算
①按内容查找
int Locate(SeqList L,ElemType e){
int i=0;
while(i<L.last&&L.elem[i]!=e){
i++;
}
if(i<=L.last)return (i+1);//找到值为e的元素,返回其序号
else return -1;//没找到,返回空序号
}
②按序号查找
int Serail(SeqList L,int n){
return L.elem[n-1];
}
③插入操作
#define OK 1
#define ERROR 0
int InsList(SeqList *L,int i,ElemType e){
int k;
if(i<1||i>L->last+2){
//插入位置不合法
return ERROR;
}
if(L->last>=MAXSIZE-1){
//表已满
return ERROR;
}
for(k=L->last;k>=i-1;k--){
L->elem[k+1]=L->elem[k];//依次后移
}
L->elem[i-1]=e;
L->last++;
return OK;
}
④删除操作
int DelList(SeqList *L,int i,ElemType *e){
int k;
if(i<1||i>L->last+1){
//删除位置不合法
return ERROR;
}
*e=L->elem[i-1];
for(k=i;k<=L->last;k++){
L->elem[k-1]=L->elem[k];//依次前移
}
L->last--;
return OK;
}
5.链表
链表中结点的逻辑顺序和物理顺序不一定相同。
由于线性表第一个元素无前驱,应设一个头指针指向第一个结点,由于线性表最后一个结点无后驱,则设为NULL;
单链表的存储结构:
typedef struct Node{
ElemType data;
struct Node* next;
}Node,*LinkList;
使用定义LinkList L,则L为头指针,指向表中第一个结点(对于带头结点的单链表,则指向单链表的头结点),若L==NULL(对于带头结点的单链表为L->next==NULL),表示单链表是一个空表。
6.链表的基本运算
①初始化单链表
void InitList(LinkList L){
*L=(LinkList)malloc(sizeof(Node));//建立头结点
(*L)->next=NULL;
}
②头插法建立单链表
void CreatFromHead(LinkList L){
//输入字符串,$是输入结束标志
Node* s;
char c;
int flag=1;
while(flag){
c=getchar();
if(c!='$'){
s=(Node*)malloc(sizeof(Node*));
s->data=c;
s->next=L->next;
L->next=s;
}else{
flag=0;
}
}
}
③尾插法建立单链表
void CreatFromTail(LinkList L){
Node *r,*s;//r指向表尾
int flag=1;
r=L;//表尾初值指向头结点
while(flag){
c=getchar();
if(c!='$'){
s=(Node*)malloc(sizeof(Node));
s->data=c;
r->next=s;
r=s;
}else{
flag=0;
r->next=NULL;
}
}
}
④按序号查找
Node* Get(LinkList L,int i){
int j=0;
Node*p=L;
if(i<=0)return NULL;
while(p->next!=NULL&&j<i){
p=p->next;
j++;
}
if(i==j)return p;
else return NULL;
}
⑤按值查找
Node* Locate(LinkList L,ElemType key){
Node* p=L->next;
while(p!=NULL)
if(p->data!=key)
p=p->next;
else break;
return p;
}
ps:两种查找方式时间复杂度均为O(n)
⑥求链表长度
int LinkLength(LinkList L){
Node* p=L->next;
int len=0;
while(p!=NULL){
p=p->next;
len++;
}
return len;
}
⑦插入操作
void InsList(LinkList L,int i,ElemType e){
Node *pre=L;Node *s;
int k=0;
if(i<=0)return ERROR;
while(pre!=NULL&&k<i-1){
pre=pre->next;
k++;
}
if(pre==NULL)return ERROR//插入位置不合法
s=(Node*)malloc(sizeof(Node));
s->data=e;
s->next=pre->next;
pre->next=s;
return OK;
}
⑧删除操作
int DelList(LinkList L,int i,ElemType *e){
Node* pre=L,*temp;
int k=0;
while(pre->next!=NULL&&k<i-1){
pre=pre->next;
k++;
}
if(pre->next==NULL)return ERROR;
temp=pre->next;
pre->next=temp->next;
*e=temp->data;
free(temp);
return OK;
}
7.循环链表
循环单链表判空条件:p!=L||p->next!+L;(p:当前结点,L:头结点)
建立循环单链表:
void CreatCList(LinkList CL){
Node*rear=CL,*s;
char c;
c=getchar();
while(c!='$'){
s=(Node*)malloc(sizeof(Node));
s->data=c;
rear->next=s;
rear=s;
c=getchar();
}
rear->next=CL;//让最后一个结点的next指向头结点
}
8.双链表
结构定义:
typedef struct DMode{
ElemType data;
struct DNode* next,*prior;
}DNode,*DoubleList;
插入操作:
int DlinkIns(DoubleList L,int i,ElemType e){
DNode *s,*p=L;
if(i<=0)return ERROR;
int k=0;
while(p->next!=NULL&k<i-1){
p=p->next;
k++;
}
if(p->next==NULL)return ERROR;
s=(DNode*)malloc(sizeof(DNode));
if(s){
s->data=e;
s->prior=p->prior;
p->prior->next=s;
s->next=p;
p->prior=s;
return OK;
}
else return ERROR;
}
删除操作:
int DlinkDel(DoubleList L,int i,ElemType *e){
DNode* p=L;
if(i<=0)return ERROR;
int k=0;
while(p->next!=NULL&k<i-1){
p=p->next;
k++;
}
if(p->next==NULL)return ERROR;
*e=p->data;
p->prior->next=p->next;
p->next->prior=p->prior;
free(p);
return OK;
}
9.静态链表
静态链表: 用数组的方式实现的链表
优点:增、删、改不需要大量移动元素
缺点: 不能随机存取,容量也是固定不变的。
10.一元多项式表示及相加
结点结构:
typedef struct Polynode{
int coef;
int exp;
struct Polynode* next;
}Polynode,*PolyList;
建立一元多项式:
//尾插法建立一元多项式
PolyList PolyCreat(){
Polynode* head,*rear,*s;
int c,e;
head=(Polynode*)malloc(sizeof(Polynode));
rear=head;
scanf("%d,%d",&c,&e);//c=0表示输入结束
while(c!=0){
s=(Polynode*)malloc(sizeof(Polynode));
s->cef=c;
s->exp=e;
rear->next=s;
rear=s;
scanf("%d,%d",&c,&e);
}
rear->next=NULL;
return head;
}
多项式相加:
void PolyAdd(PolyList G,PolyList X){
Polynode *p,*q,*tail,*temp;
int sum=0;
p=G->next;
q=X->next;
tail=G;
while(p!=NULL&&q!=NULL){
if(p->exp<q->exp){
tail->next=p;
tail=p;
p=p->next;
}else if(p->exp==q->exp){
sum=p->coef+q->coef;
if(sum!=0){
p->coef=sum;
tail->next=p;
tail=p;
p=p->next;
temp=q;q=q->next;free(temp);
}
else{
temp=p;p=p->next;free(temp);
temp=q;q=q->next;free(temp);
}
}else{
tail->next=q;
tail=q;
q=q->next;
}
}
if(p!=NULL)tail->next=p;
else ail->next=q;
}
11.在带头结点的非空链表中,头结点的存储位置由头指针指示,首元素结点的存储位置由头结点的指针域指示,除首元素结点外,其他任一元素结点的存储位置由其前驱指针域指示。
12.设顺序表L中有n个数据元素,则删除该表中第i个元素需要移动n-i个元素。
第三章-限定性线性表(栈与队列)
1.顺序栈
#define Stack_Size 50
typedef struct {
StackElementType elem[Stack_Size];
int top;
}SeqStack;
①初始化空栈:
void InitStack(SeqStack *S){
S->top=-1;
}
②进栈
int Push(SeqStack *S,StackElemrntType x){
if(S->top==Stack_Size-1) return False;//栈满
S->top++;
S->elem[S->top]=x;
return TRUE;
}
③出栈
int Pop(SeqStack *S,StackElementType *x){
if(S->top==-1)return FALSE;
else{
*x=S->elem[S->top];
S->top--;
return TRUE;
}
}
④读栈顶元素
int GetTop(SeqStack *S,StackElementType *x){
if(S->top==-1)return FALSE;//栈空
else{
*x=S->elem[S->top];
return TRUE;
}
}
⑤多栈共享技术
#define M 100
typedef struct {
StackElementType Stack[M];
StackElementType top[2];
}DqStack;
双端栈初始化
void InitStack(DqStack *S){
S->top[0]=-1;
S->top[1]=M;
}
双端栈进栈
int Push(DqStack *S,StackElementType x,int i){
if(S->top[0]+1==S->top[1])return FALSE;//栈满
switch(i){
case 0:
S->top[0]++;
S->Stack[S->top[0]]=x;
break;
case 1:
S->top[1]--;
S->Stack[S->top[1]]=x;
break;
default:return FALSE;
}
return TRUE;
}
双端栈出栈
int Pop(DqStack *S,StackElementType *x,int i){
switch(i){
case 0:
if(S->top[0]==-1)return FALSE;
*x=S->Stack[S->top[0]];
S->top[0]--;
case 1:
if(S->top[1]==M)return FALSE;
*x=S->Stack[S->top[1]];
S->top[1]++;
default:return FALSE;
}
return TRUE;
}
2.链栈
typedef struct node{
StackElementType data;
struct node* next;
}LinkStackNode;
typedef LinkStackNode* LinkStack;
①进栈
int Push(LinkStack top,StackElementType x){
LinkStackNode*temp=(LinkStackNode*)malloc(sizeof(LinkStackNode));
if(temp==NULL)return FALSE;//申请空间失败
temp->data=x;
temp->next=top->next;
top->next=temp;
return TRUE;
}
②出栈
int Pop(LinkStack top,StackElementType *x){
LinkStackNode*temp=top->next;
if(temp==NULL)return FALSE;//栈空
top->next=temp->next;
*x=temp->data;
free(temp);
return TRUE;
}
③括号匹配算法
//每读入一个括号,若是左括号,直接入栈。若读入右括号,且与栈顶左括号同类型,则栈顶左括号出栈
void BracketMatch(char *str){
Stack S;int i;char ch;
InitStack(&S);
for(=0;str[i]!='\0';i++){
switch(str[i]){
case '(':
case '[':
case '{':
Push(&S,str[i]);
break;
case ')':
case ']':
case '}':
if(IsEmpty(&S)) return ;//右括号多余
else{
GetTop(&S,&ch);
if(Match(ch,str[i]))Pop(&S,&ch);//判断两个括号是否匹配
else return ;//对应左右括号不匹配
}
}
}
if(IsEmpty(S))printf("括号匹配");
else printf("左括号多余");
}
④无括号算术表达式处理算法
规定运算符优先级,设置两个栈分别存储运算符和运算数;若遇到运算数则进OVS栈,若遇到运算符则与栈顶运算符进行优先级比较,如果当前运算符优先级大于栈顶的,则入OPTR栈;若当前运算符优先级小于等于栈顶的,则OPTR出栈一次,得到运算符*,OVS出栈两次,得到操作数a和b,对a,b进行*运算,得到结果入OVS栈。
int ExpEvaluation(){
InitStack(&OPTR);
InitStack(&OVS);
Push(&OPTR,'#');
//输入表达式,以#为结尾
ch=getchar();
while(ch!='#'||GetTop(OPTR)!='#'){
if(!In(ch,OPset)){//不是操作符,进OVS栈
n=GetNumber(ch);
push(&OVS,n);
ch=getchar();
}
else{
switch(Compare(ch,GetTop(OPTR))){
case '>':Push(&OPTR,ch);
ch=getchar();break;
case '=':case '<':Pop(&OPTR,&op);
Pop(&OVS,&b);
Pop(&OVS,&a);
v=Excute(a,op,b);//对a和b进行op运算
Push(&OVS,v);
break;
}
}
}
v=GetTop(OVS);
return v;
}
3.链队列
①定义:
typedef struct Node{
QueueElementType data;
struct Node* next
}LinkQueueNode;
typedef struct{
LinkQueueNode* front;
LinkQueueNode* rear;
}LinkQueue;
②初始化
int InitQueue(LinkQueue* Q){
Q->front=(LinkQueueNode*)malloc(sizeof(LinkQueueode));
if(Q->front!=NULL){
Q->rear=Q->front;
Q->front->next=NULL;
return TRUE;
}
else return FALSE;
}
③链队列入队
int EnterQueue(LinkQueue* Q,QueueElementType x){
LinkQueueNode *NewNode;
NewNode=(LinkQueueNode*)malloc(sizeof(LinkQueueNode));
if(NewNode!=NULL){
NewNode->data=x;
NewNode->next=NULL;
Q->rear->next=NewNode;
Q->rear=NewNode;
return True;
}
else return FALSE;
}
④链队列出队
int DeleteQueue(LinkQueue* Q,QueueElementType *x){
LinkQueueNode* p;
if(Q->front==Q->rear)return FASLE;//队列为空
p=Q->front->next;
Q->front->next=p->next;//队头元素出队
if(Q->rear==p)//如果队中只有一个元素,出队后成为空队
Q->rear=Q->front;
*x=p->data;
free(p);
return TRUE;
}
4.循环队列
循环队列是队列的一种顺序表示和实现方法。
①定义
#define MAXSIZE 100
typedef struct{
QueueElementType element[MAXSIZE];
int front;
int rear;
}SeqQueue;
②初始化
void InitQueue(SeqQueue *Q){
Q->front=Q->rear=0;
}
③循环队列入队操作
int EnterQueue(SeqQueue* Q,QueueElementType x){
if((Q->rear+1)%MAXSIZE==Q->front)//尾指针加一追上头指针,队列已满
return FALSE;
Q->element[Q->rear]=x;
Q->rear=(Q->rear+1)%MAXSIZE;
return TRUE;
}
④循环队列出队操作
int DeleteQueue(SeqQueue* Q,QueueElementType* x){
if(Q->rear==Q->front)return FALSE;//队列为空
*x=Q->element[Q->front];
Q->front=(Q->front+1)%MAXSIZE;
return TRUE;
}
5.习题
①主机将要打印输出的数据依次写入缓冲区,而打印机则依次从缓冲区中取出数据,该缓冲区的逻辑结构应该是:__队列_____
②某队列允许在其两端进行入队操作,但仅允许在一端进行出队操作,元素a,b,c,d,e依次入队,则不可能的出队顺序是:___C__
A。bacde B。dbace C。dbcae D。ecbad
ps:在原序列中相对位置比它靠前的,也就是比它先入栈的,出栈顺序必须是逆序。在原序列中相对位置比它大的,也就是比它后入栈的,出栈顺序没有要求;以选项中出栈的第一个元素为基准,判断它后面的元素是否满足上述规律
③设输入序列是1,2,3,4........,n,经过栈的作用后输出序列的第一个元素是n,则输出序列中第i个输出元素是___n+1-i_____
④设循环队列Q[0:M-1]的队头指针和队尾指针分别为F和R,队头指针F总是指向队头元素,队尾指针R总是指向队尾元素的下一个位置,则该循环队列中的元素个数为___(R-F+M)%M______
第四章-串
1.定长顺序串
①定义
#define MAXLINE 100
typedef struct{
char ch[MAXLINE];
int len;
}SString;
②顺序串插入操作
int StrInsert(SString *s,int opt,SString t){
int i;
if(pos<0||pos>s->len)return 0;//插入位置不合法
if(s->len+t.len<=MAXLEN){//插入后串长<=MAXLINE
for(i=s->len+t.len-1;i>=t.len;i--){
s->ch[i]=s->ch[i-t.len];
}
for(i=0;i<t.len;i++){
s->ch[i+pos]=t.ch[i];
}
s->len+=t.len;
}
else if(pos+t.len<=MAXLINE){//插入后串长>MAXLINE,但t可以全部插入
for(i=MAXLINE-1;i>t.len+pos-1;i--){
s->ch[i]=s->ch[i-t.len];
}
for(i=0;i<t.len;i++){
s->ch[i+pos]=t.ch[i];
}
s->len=MAXLINE;
}
else{//插入后串长>MAXLINE
for(i=0;i<MAXLINE-pos;i++){
s->ch[i+pos]=t.ch[i];
}
s->len=MAXLINE;
}
return 1;
}
③顺序串删除操作
int StrDelete(SString *s,int pot,int len){
int i;
if(pos<0||pos>s->len-len)return 0;//删除参数不合法
for(i=pos+len;i<s->len;i++){
s->ch[i-len]=s->ch[i];
}
s->len=s->len-len;
return 1;
}
④串比较操作
int StrCompare(SString s,SString t){//若s>t则返回正数,若s<t则返回负数
int i;
for(i=0;i<s.len&&i<t.len;i++){
if(s.ch[i]!=t.ch[i])return (s.ch[i]-t.ch[i]);
}
return (s.len-t.len);
}
⑤顺序串简单模式匹配操作
int StrIndex(SString s,int pot,SString t){
//求串s从下标pot开始,子串t第一次出现的位置,不成功返回-1.
int i,j,start;
if(t.len==0)return 0;//空串匹配任意串
start=pos;i=start;j=0;
while(i<s.len&&j<t.len){
if(s.ch[i]==t.ch[j]){
i++;j++;
}
else{
start++;
i=start;j=0;
}
}
if(j>=t.len)return start;
else return -1;
}
补充:KMP算法(字符串匹配):失配后无回溯,匹配速度高
#include <iostream>
using namespace std;
const int N = 100010, M = 1000010;
int n, m;
int ne[N];
char s[M], p[N];
int main()
{
cin >> n >> p + 1 >> m >> s + 1;
for (int i = 2, j = 0; i <= n; i ++ )
{
while (j && p[i] != p[j + 1]) j = ne[j];
if (p[i] == p[j + 1]) j ++ ;
ne[i] = j;
}
for (int i = 1, j = 0; i <= m; i ++ )
{
while (j && s[i] != p[j + 1]) j = ne[j];
if (s[i] == p[j + 1]) j ++ ;
if (j == n)
{
printf("%d ", i - n);
j = ne[j];
}
}
return 0;
}
2.堆串
定义:ch指示串的起始地址。
typedef struct{
char *ch;
int len;
}HString;
3.块链串
定义:一个链表存放一个串值,每个结点可以存放一个或多个字符。每个结点成为块。
#define BLOCK_SIZE 4;
typedef struct{
char ch[BLOCK_SIZE];
struct Block* next;
}Block;
typedef struct{
Block* head;
Block* tail;
int len;
}BLString;
第五章-数组和广义表
1.数组地址计算
一维数组:Loc(A[i])=Loc(A[1])+(i-1)*size。
二维数组:假设以行为主存放,下标从1开始,首元素a11地址为Loc(A[1][1]),每个元素占size个存储单元
Loc(A[i][j])=Loc(A[1][1])+(n*(i-1)+j-1)*size。
三维数组:Loc(A[i][j][k])=Loc(A[1][1][1])+((i-1)*m*n+(j-1)*n+(k-1))*size。
2.特殊矩阵的压缩存储
①三角矩阵(上三角矩阵,下三角矩阵,对称矩阵)
下三角矩阵元素个数为n*(n+1)/2.
即,将下三角矩阵的非零元存储到一个大小为n*(n+1)/2的一维数组中。
下标为:Loc[i,j]=Loc[1,1]+(i*(i-1)/2+j-1):
Loc[1,1]+前i-1行非零元素个数+第i行中aij前的非零元素个数
对称矩阵满足aij=aji,可以为每一对相等的元素分配一个存储空间,即只存下三角或上三角矩阵。从而将n^2个元素压缩到n*(n+1)/2个空间中。k为压缩后的数组下标。②
方案一:存储对角线及其以下元素,按行序存储:
当i>=j时,k=i(i-1)/2+j。
当i<j时,k=j(j-1)/2+i。//该元素未存储,但[j][i]单元的元素即为该元素。
方案二:存储对角线及其以上元素,按行序存储:
当i<=j时,k=j(j-1)/2+i。
当i>j时,k=i(i-1)/2+j。//该元素未存储,但[i][j]单元的元素即为该元素。
②带状矩阵
在矩阵中,所有非零元都集中在以主对角线为中心的带状区域中,最常见的是三角带状矩阵
特点:
当i=1时,j=1,2;
当1<i<n时,j=i-1,i,i+1;
当i=n时,j=n-1,n。
三角带状矩阵可以压缩到大小为3n-2的一维数组中。
Aij在一维数组中的地址:
Loc(A[i][j])=Loc(A[1][1])+(3*(i-1)-1+j-i+1)*size.
Loc(A[1][1])+(前i-1行非零元素个数+第i行中aij前非零元素个数)*size。
③稀疏矩阵
稀疏矩阵是指矩阵中大多数元素为0的矩阵。稀疏矩阵的三元组表表示法:
定义
#define MAXSIZE 100
typedef struct{
int row,col;
ElementType e;
}Triple;
typedef struct{
Triple data[MAXSIZE];//非零元素的三元组表
int m,n,len;//矩阵的行数,列数,非零元个数。
}TSMatrix;
稀疏矩阵转置经典算法:
void TransMatrix(ElementType source[m][n],ElementType dest[m][n]){
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
dest[j][i]=source[i][j];
}
}
}
3.广义表
①举例:
D=();//空表
A=(a,(b,c))//表长为2的广义表,第一个元素是单个数据a,第二个元素是子表(b,c)
B=(A,A,D)//长度为3的广义表,前两个元素为表A,第三个元素为空表D。
C=(a,C)//长度为2递归定义的广义表,C相当于无穷表C=(a,(a,(a,(...))))
以表A为例,head(A)=a,表示A的表头是a,tail(A)=((b,c)),表示A的表尾是
((b,c)),广义表的表尾一定是一个表。
e.g:广义表A=(a,(b,(c))),那么Tail(Head(Tail(A)))是:((c))
对于广义表来说,难以用顺序存储结构来表示他,通常用链式存储结构来表示。
②链表存储结构:
Ⅰ头尾链表结点结构:
表中有两类结点:单个元素结点,子表结点。
①中的A,B,C,D的头尾链存储结构
Ⅱ同层结点链存储结构:
结点均由三个域组成:
①中的A,B,C,D的同层结点链存储结构:
第六章-树与二叉树
1.树
①树的相关术语:
结点的度:一个结点的子树个数
叶结点:度为0的结点,即无后继的结点,也称终端节点。
分支结点:度不为0的结点。
结点的层次:从根结点开始,根结点的层次为1,根的直接后继层次为2,以此类推。
树的度:树中所有结点的度的最大值。
树的高度:树中所有结点的层次的最大值。
森林:将一棵非空树的根结点删去,树就变成一个森林;反之,给森林增加一个统一的根结点,森林就变成一棵树。
同构:如果树A通过若干次左右子树替换得到树B,就称树A和树B同构。
2.二叉树
任何树都可以转化为二叉树进行处理。
①二叉树的性质:
Ⅰ.在二叉树第i层至多有2^(i-1)个结点(i>=1);
Ⅱ.深度为k的二叉树之多有2^k-1个结点。
Ⅲ.对于任一二叉树,叶子结点数为n0,度数为2的结点数为n2,则n0=n2+1.
Ⅳ.具有n个结点的完全二叉树深度为[logn]+1.
Ⅴ.对于有n个结点的顺序存储二叉树,结点从1开始编号,对于任意编号为i的结点有:
若i=1,则是根节点,无双亲结点,若i>1,双亲结点的序号是[i/2];
左孩子编号是2*i,右孩子编号是2*i+1。
Ⅵ.n个结点的二叉树,共2n个指针域,其中n-1个非空指针域,n+1个空指针域。
②二叉树顺序存储结构
可以将二叉树的数据元素逐层放入数组中。对于非完全二叉树来说,必须用“虚结点”将其补成完全二叉树。这个行为就会造成一定浪费。
3.二叉树链式存储结构
设计每个结点至少包括三个域:数据域,左孩子域,右孩子域。
①结点结构定义:
typedef struct{
DataType data;
struct Node* LChild;
struct Node* RChild;
}BiTree,*BiTree;
有时,为了便于找到双亲结点,可以增加一个Parent域
4.二叉树的遍历
①先序遍历
void PreOrder(BiTree root){
if(root!=NULL){
Visit(root->data);
PreOrder(root->LChild);
PreOrder(root->RChild)
}
}
②中序遍历
void InOrder(BiTree root){
if(root!=NULL){
InOrder(root->LChild);
Visit(root->data);
InOrder(root->RChild);
}
}
③后序遍历
void PostOrder(BiTree root){
if(root!=NULL){
PostOrder(root->LChild);
PostOrder(root->RChild);
Visit(root->data);
}
}
这些二叉树递归遍历算法的时间复杂度为O(n)。
④先序遍历输出二叉树中的叶子结点
void PreOrder(BiTree root){
if(root!=NULL){
if(root->LChild!=NULL&&root->RChild!=NULL)printf(root->data);
PreOrder(root->LChild);
PreOrder(root->RChild);
}
}
⑤用扩展先序遍历创建二叉链表
void CreatBiTree(BiTree* root){
char ch;
ch=getchar();
if(ch=='.')*root=NULL;
else{
*root=(BiTree)malloc(sizeof(BiTree));
(*root)->data=ch;
CreatBiTree(&((*root)->LChild));
CreatBiTree(&((*root)->RChild));
}
}
⑥后序遍历求二叉树高度
int PostOrder(BiTree root){
int hl,hr,max;
if(root!=NULL){
hl=PostOrder(root->LChild);
hr=PostOrder(root->RChild);
max=hl>hr?hl:hr;
return max+1
}else return 0;
}
⑦中序遍历非递归(调用栈操作的函数)
void InOrder(BiTree root){
InitStack(&S);
p=root;
while(p!=NULL||!IsEmpty(S)){
if(p!=NULL){
Push(&S,p);
p=p->LChild;
}
else{
Pop(&S,&p);Visit(p->data);
p=p->RChild;
}
}
}
⑧中序遍历非递归(直接实现栈操作)
void InOrder(BiTree root){
//data[m]表示栈,top表示栈顶指针
top=0;
p=root;
do{
while(p!=NULL){
if(top>m)return ;
top++;
data[top]=p;
p=p->LChild;
}
if(top!=0){
p=data[top];
top--;
Visit(p->data);
p=p->RChild;
}
}while(p!=NULL||top!=0)
}
⑨后序遍历非递归(调用栈的操作函数)
void PostOrder(BiTree root){
BiTNode* p,*q;
Stack S;
q=NULL;
p=root;
InitStack(&S);
while(p!=NULL||!IsEmpty(S)){
if(p!=NULL){
Push(&S,p);
p=p->LChild;
}
else{
GetTop(&S,&p);
if((p->RChild==NULL)||(p->RChild==q)){
visit(p->data);
q=p;
Pop(&S,&p);
p=UNLL;
}
else{
p=p->RChld;
}
}
}
}
5.线索二叉树
①基本概念
充分利用二叉链表中的空链域,将遍历过程中的结点前驱,后继的信息保存下来。可以利用空闲的n+1个空链域来存放:若结点有左子树,则LChild指向其左孩子,否则LChild指向其前驱结点;若结点有右子树,则RChild指向其右孩子,否则RChild指向其后继结点。为了区分孩子结点和前驱后继结点,为结点结构增加两个标志域:Ltag=0时,LChild指向左孩子,Ltag=1时,LChild指向其前驱结点;Rtag=0时,RChild指向其右孩子,Rtag=1时,RChild指向其后继结点。在这种存储结构中,指向前驱和后继结点的指针称为线索。
二叉树的线索化实质上是将二叉链表中的空指针域填上相应节点的前驱和后继。因此线索化的过程即在遍历的过程中修改空指针域的过程。对于不同的遍历次序可以得到不同的线索二叉树。
②建立中序线索树
void Inthread(BiTree root){
BiTree pre=NULL;
if(root!=NULL){
Inthread(root->LChild);
if(root->LChild==NULL){
root->Ltag=1;
root->LChild=pre;//置前驱线索
}
if(pre!=NULL&&pre->RChild==NULL){
pre->Rtag=1;
pre->RChild=root;//置后继线索
}
pre=root;
Inthread(root->RChild);
}
}
③在中序线索树中找结点前驱
BiTNode* InPre(BiTNode* p){
if(p->Ltag==1)pre=p->LChild;
else{//在左子树中找最右下端结点
for(q=p->LChild;q->Rtag==0;q=q->RChild)
pre=q;
}
return pre;
}
④在中序线索树中找结点后继
BiTNode* InNext(BiTNode* p){
if(p->Rtag==1)Next=p->RChild;
else{//在p的右子树中查找最左下端结点
for(q=p->RChild;q->Ltag==0;q=q->LChild)
Next=q;
}
return Next;
}
⑤在中序线索树上求中序遍历的第一个结点
BiTNode* InFirst(BiTree root){
BiTNode* p=root;
if(!p)return NULL;
while(p->Ltag==0)p=p->LChild;
return p;
}
⑥遍历中序线索树
void InOrder(BiTree root){
BiTNode* p=InFirst(root);
while(p){
visit(p);
p=InNext(p);
}
}
⑦已知两种遍历序列,求二叉树:
已知后序和中序:后序的最后一个结点就是根结点,中序的根结点左边是左子树,右边是右子树,然后递归进行前面的操作即可。
已知前序和中序:前序的第一个结点就是根结点,其余同上。
已知后序和前序:无法求得唯一二叉树。
6.树,森林,二叉树的关系
①将树转换成二叉树:
树中所有兄弟之间加一条连线;对树中每个结点,只保留其与第一个孩子之前的连线,删去其与其他孩子的连线。
②将森林转换成二叉树:
先将森林中的每棵树转换成相应的二叉树
第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树根结点的右孩子。
③二叉树还原为树或森林:
若某结点是其双亲的左孩子,则把该结点的右孩子,右孩子的右孩子……都与该结点的双亲结点用线连起来;删掉二叉树中所有双亲结点与右孩子结点的连线;
7.哈夫曼树
①基本概念
路径:从根结点到该结点的分支序列。
路径长度:是指根结点到该结点所经过的分支数目。
结点的权:人们常常给树的结点赋予一个具有某种实际意义的实数。
结点的带权路径长度:从根结点到某一结点的路径长度与该结点的权的乘积。
树的带权路径长度:树中从根到所有叶子结点的各个带权路径长度之和,记为WPL=Wi*Li。
②构造哈夫曼树
哈夫曼树是由n个带权叶子结点构成的所有二叉树中带权路径长度最短的二叉树,又称最优二叉树
用静态三叉链表实现哈夫曼树的类型定义
#define N 20
#define M 2*N-1
typedef struct{
int weight;//结点的权值
int parent;//双亲下标
int LChild;//
int RChild;
}HTNode,HuffmanTree[M+1]; //Huffman树是一个结构数组,0号单元不用
创建哈夫曼树
void CreatHuffmanTree(HuffmanTree ht,int w[],int n){
for(int i=1;i<=n;i++){
ht[i]={w[i],0,0,0}; //1~n号单元存放叶子结点
}
int m=2*n-1;
for(int i=n+1;i<=m;i++){
ht[i]={0,0,0,0};//n+1~m号单元存放非叶子结点
}
//------------------------初始化完成
for(int i=n+1;i<=m;i++){
select(ht,i-1,&s1,&s2);//在ht[1]~ht[i-1]选择两个parent为0且weight最小的结点,序号赋值给s1,s2.
ht[i].weight=ht[s1].weight+ht[s2].weight;
ht[s1].parent=i;
ht[s2].parent=i;
ht[i].LChild=s1;
ht[i].RChild=s2;
}
}
哈夫曼编码:
对于一棵具有n个叶子的哈夫曼树,若对树中每个左分支赋予0,右分支赋予1,则从根到每个叶子的通路上,各分支的赋值分别构成一个二进制串,该二进制串就称为哈夫曼编码。
相关特性:
哈夫曼编码是前缀编码。哈夫曼编码是最优前缀编码。
求哈夫曼树的哈夫曼编码:
void CHuffmanTree(HuffmanTree ht,HuffmanCode hc,int n){
//从叶子结点到根,逆向求哈夫曼编码
char *cd;
cd=(char*)malloc(n*sizeof(char));
cd[n-1]='\0';//从右向左存放编码,先放结束符
for(int i=1;i<=n;i++){
start=n-1;//起始指针
c=i;p=ht[i].parent;
while(p!=0){
--start;
if(ht[p].LChild==c)cd[start]='0';//左分支
else cd[start]='1';//右分支
c=p;p=ht[p].parent;//向上倒推
}
hc[i]=(char*)malloc((n-start)*sizeof(char));
strcpy(hc[i],&cd[start]);//把编码复制到hc[i]中
}
free(cd);
}
8.习题
①已知一棵度为k的树中有n1个度为1的结点,n2个度为2的结点,……,nk个度为k的结点,则该树中有多少叶子结点?
答:设该树中的叶子数为n0个.该树中的总结点数为n个;
则有 n=n0+n1+n2+…+nK (1)
n-1=0×n0+1×n1+2×n2+…+K×nK (2)
联立(1)(2)方程组可得 叶子数为:n0=1+0×n1+1×n2+2×n3+……+(K-1)×nK
②已知二叉树有50个叶子结点,则该二叉树总结点数至少多少个?
答:设n0表示叶子结点数,n2表示度为2的结点数;
则n0 = n2+1 所以n2= n0 -1=49,当二叉树中没有度为1的结点时,总结点数 n=n 0+ n2=99
③计算:
Ⅰ.n个结点的k叉树,若用具有k个child域的等长链结点存储树的一个结点,则空的child域有多少个?
答:总的child域为:n*k
分支总数:n-1
所以非空child域是:n*k-(n-1)=n*(k-1)+1。
Ⅱ.一棵完全二叉树共有520个结点,该完全二叉树有多少个叶子结点,度为1的结点,度为2的结点?
答:因为520是偶数,所以n1=1.
由n0=n2+1,可得n0=260,n2=259.
ps:
满二叉树的所有节点的度都是2或者0,没有度为1的节点。
完全二叉树,可以看做是满二叉树在最后一层从右往左砍掉一些节点。
如果从满二叉树中在最后一层自左向右砍掉的节点数是偶数,那么该完全二叉树中度为1的节点数就是0。
如果砍掉的节点数是奇数,那么该完全二叉树中就有且仅有一个节点的度为1.
Ⅲ.已知完全二叉树第7层有10个结点,则整个二叉树的结点数为多少个?
答:第六层是满的,前6层结点数:2^6-1=63.
总数:63+10=73.
Ⅳ.不同的线索化二叉树张,空余指针个数分别是多少?
答:第一个结点没有前驱,则其左指针为空,最后一个结点没有后继,则其右指针为空.
因此在不同的线索化二叉树中,空余指针个数应该是两个.
④已知一棵二叉树按顺序方式存储在数组A[1..n]中,设计算法求出下标分别为i和j的两个结点的最近的公共结点祖先的值。
void Ancestor(ElemType A[],int n,int i,int j){
while(i!=j){
if(i>j)i/=2;
else j/=2;
}
printf("最近的公共祖先的下标是:%d,值是:%d",i,A[i]);
}
⑤假设采取二叉链表的方式存储二叉树,root指向根结点,p所指向结点和q所指向结点是二叉树中的两个结点,编写一个函数计算他们最近的公共祖先。
答:采用非递归后序遍历,假设p在q左边。当后序遍历访问到p时,此时St中所有结点均为结点p的祖先,将其复制到anor中,继续后序遍历访问到q时,情况同p,将其与anor中结点依次比较,找出最近的共同祖先。
int ancestor(BTNode* root,BTNode* p,BTNode* q){
BTNode* St[MAXSIZE];
BTNode* temp;
ElemType anor[MAXSIZE];
int i,flag,top=-1;
do{
while(root){//将t的所有左结点入栈
top++;
St[top]=root;
root=root->LChild;
}
temp=NULL;//p指向前一个已访问过的结点
flag=1;
while(top!=-1&&flag){
root=St[top];//
if(t->RChild==temp){
if(root==p){
for(i=0;i<=top;i++)
anor[i]=St[i]->data;
top--;
temp=root;
}
else if(root==q){
i=0;
while(anor[i]==St[i]->data)
i++;
cout<<"最近公共祖先:"<<anor[i-1]<<endl;
return i;
}
else{
top--;
temp=root;
}
}
else{
root=root->RChild;
flag=0;
}
}
}while(top!=-1);
return 0;
}
⑥编写递归算法:对于二叉树中每一个元素为x的结点,删去以它为根的子树,释放相应空间。
void Del_x(BiTree T,int x){
if(T->data==x)Del_sub(T);
else{
if(T->LChild)Del_x(T->LChild);
if(T->RChild)Del_x(T->RChild);
}
}
void Del_sub(BiTree T){
if(T->LChild)Del_sub(T->LChild);
if(T->RChild)Del_sub(T->RChild);
free(T);
}
⑦写算法判别一棵二叉树是否为一棵正则二叉树(二叉树中不存在子树个数为1的结点)
bool IsNormalTree(BitTree *T)
{
if(!T)
return true;
if(!T->LChild&&!T->RChild)
return true;
else if(T->LChild&&T->RChild)
{
if(IsNormalTree(bt->lchild)&&IsNormalTree(bt->rchild))
return true;
else
return false;
}
else
return false;
}
⑧计算二叉树的最大宽度(二叉树所有层中结点个数的最大值)
int MWidth(BiTree T)
{
if(T==NULL) return 0;
Queue Q;
int temp = 0, last = 0, max = 0;
BiTree t = T;
InitQueue(&Q);
Push(&Q, &t);
while (Q.front <= last){
OutQueue(&Q, &t);
temp++;
if (t->LChild != NULL){
Push(&Q, &t->LChild);
}
if (t->RChild != NULL){
Push(&Q, &t->RChild);
}
if (Q.front > last){ //一层结点全部遍历完成,开始判断最大宽度
last = Q.rear-1;
max = max > temp ? max : temp;
temp = 0;
}
}
return max;
}
⑨二叉链表方式存储二叉树,编写算法将左右子树进行交换。
void exchange(BiTree T){
BiNode *r,*p,*stack[MAXSIZE];
int top=0;
stack[top++]=T;
while(top>0){
p=stack[--top];
if(p){
r=p->LChild;
p->LChild=p->RChild;
p->RChild=r;
stack[top++]=p->LChild;
stack[top++]=p->RChild;
}
}
}