数据结构の整理
第一章 绪论
1.1基本概念和术语
1.1.1基本概念
数据:所有能输入到计算机中进行加工处理的对象。
数据元素:组成数据的基本单位。是数据集合的个体,在计算机中通常作为整体进行处理。也称为结点或记录。
数据项:有独立含义的数据最小单位,也称项或字段。
三者之间的关系:数据>数据元素>数据项
1.2数据结构
数据结构包含的三方面内容:
①:数据的逻辑结构(独立于计算机,本身固有)
②:数据的存储/物理结构(逻辑结构在计算机中的映像,必须依赖计算机)
③:数据结构的操作/运算
逻辑结构的4类基本结构:
①集合 ;② 线性结构:一对一;③ 树形结构:一对多;④图状结构:任意前驱后继
存储结构的两类结构:
①顺序储存结构 ②链式储存结构(利用指针)
逻辑结构与存储结构的关系:算法设计—>逻辑结构;算法实现—>存储结构
1.3算法
算法:是规则的有限集合,是为解决特定问题而规定的一系列操作。
算法的特性:1.有穷性 2.确定性 3.输入 4.输出 5.可行性
算法设计的要求:正确性,可读性,健壮性,高效率和低存储量
对算法作性能的评价:时间复杂度(循环次数)和空间复杂度(借助的辅助空间)
上图中题目2-10连续存储设计地址一定连续;
2-11算法的时间复杂度取决于问题的规模和待处理数据的初态
第二章 线性表
2.1顺序存储结构
1.顺序存储结构:用一组地址连续的存储单元按线性表的元素间的逻辑顺序依次存放数据元素
2.顺序存储的实现(初始化):
2.2顺序表的运算
1.查找:
①顺序查找:遍历顺序表。O(n) ②按位置查找:通过下标直接查找。O(1)
int LocateElem(SqList L,ElemType e)
{
int i;
for ( i = 0; i < L.length; i++ )
{
if (L.elem[i]==e)
{
return i+1;
}
}
return 0;
}
2.插入:实现判断插入位置 i 是否合法,表是否已满。O(n)
顺序插入
int SqInsert(SqList &L,ElemType e)
{
int insert=L.length;
if(L.length==MAXSIZE) return 0;
for(int i=L.length-1;i>=0&&L.elem[i]>e;i--)
{
L.elem[i+1] = L.elem[i];
insert=i;
}
L.elem[insert] = e;
L.length++;
return 1;
}
表满时使用realloc函数:指针名=(数据类型)realloc(要改变内存大小的指针名,新的大小)。*
3.删除:删除后要移动数据,与插入时间复杂度相同O(n)
int ListDelete(SqList &L,int i)
{
if(i<1 || i>L.length)
{
return 0;
}
for (int j=i;j<L.length;j++)
{
L.elem[j-1]=L.elem[j];
}
L.length--;
return 1;
}
2.3链式存储结构
1.
2.链式存储的结构特点:其结点在存储器中的位置是随意的,即逻辑上相邻的数据元素,在物理上不一定相邻。
3.相关术语:
4.
2.4链式存储的运算
1.
int insert_link ( LinkList L,int i,ElemType e)
{
int j;
LNode *in,*p;
in=(LNode*)malloc(sizeof(LNode));
p=L;
for ( j = 1;p!=NULL && j < i; j++ )
{
p=p->next;
}
if (p==NULL || i<=0)
{
return 0;
}
in->data=e;
in->next=p->next;
p->next=in;
return 1;
}
2.
int delete_link ( LinkList L,int i)
{
int j;
LinkList p;
p=L;
for ( j = 1; p!=NULL &&j < i; j++ )
{
p=p->next;
}
if (p->next==NULL || i<=0)
{
return 0;
}
p->next=p->next->next;
return 1;
}
3.
List Merge( List L1, List L2 )
{
PtrToNode fir,sec,thi,l;
l=(struct Node*)malloc(sizeof(struct Node));
fir=L1->Next;
sec=L2->Next;
thi=l;
while(fir!=NULL&&sec!=NULL)//排序能比较的部分
{
if (fir->Data<=sec->Data)
{
thi->Next = fir;
thi=thi->Next;
fir= fir->Next;
}else
if (fir->Data>sec->Data)
{
thi->Next = sec;
thi = thi->Next;
sec = sec->Next;
}
}
if (fir!=NULL&&sec==NULL)//将多余的部分直接接入表尾
{
thi->Next=fir;
}else
if (sec!=NULL&&fir==NULL)
{
thi->Next=sec;
}
L1->Next = NULL;
L2->Next = NULL;
return l;
}
4一元多项式(难点)
#include <stdio.h>
#include <stdlib.h>
typedef struct List
{
int coef;//系数
int expon;//指数
struct List *next;
}List,*list;
list creat()//创建链表,尾插
{
list head,last,p;
char c;
int num1,num2;
head=(struct List*)malloc(sizeof(struct List));
last=head;
do
{
p=(struct List*)malloc(sizeof(struct List));
scanf("%d %d", &num1,&num2);
p->coef=num1;
p -> expon = num2;
p->next=NULL;
last->next=p;
last=p;
}while (c=getchar()!='\n');//不读到换行不终止循环
return head;
}
list Dao(list l1)//求导函数
{
list head,last,mul,p;
head=(struct List*)malloc(sizeof(struct List));
p=l1->next;
last=head;
if (p->expon==0)
{
mul=(struct List*)malloc(sizeof(struct List));
mul->coef=0;
mul->expon=0;
last->next=mul;
mul->next=NULL;
return head;
}else
while (p!=NULL&&p->expon!=0)
{
mul=(struct List*)malloc(sizeof(struct List));
mul->coef=(p->coef)*(p->expon);
mul->expon=p->expon-1;
last->next=mul;
last=mul;
mul->next=NULL;
p=p->next;
}
return head;
}
void Print(list l2)//输出函数
{
int count=0;
list p=l2->next;
while(p!=NULL)
{
if(count==0){
printf("%d %d", p->coef,p->expon);
count++;
}else
printf(" %d %d", p->coef,p->expon);
p=p->next;
}
}
int main()
{
list l1,l2;
l1=creat();
l2=Dao(l1);
Print(l2);
return 0;
}
2.5线性表的一些题目
1.
正确答案为D,如是单项循环链表,在把尾指针删除需要遍历一次才能得到尾节点的上一个节点,然后进行前移,每次都是O(n)。
2.
应当反过来,访问为O(N),增加为O(1)
3.
头结点的存储位置由头指针指示
4.
顺序表是随机存储结构
5.
第三章 栈和队列
3.1栈的定义与特点
1.定义:栈是一种特殊的线性表,只允许在线性表的同一段进行插入和删除.
2.栈与一般线性表的区别
3.栈的存储:
3.2栈的操作与应用
4.栈的基本操作
①栈的初始化:
②入栈:
bool Push( Stack S, ElementType X )
{
if (S->Top==S->MaxSize)
{
printf("Stack Full\n");
return false;
}else
{
S->Data[S->Top]=X;
S->Top++;
return true;
}
}
③出栈:
ElementType Pop( Stack S )
{
if(S->Top==0)
{
printf("Stack Empty\n");
return ERROR;
}else
{
S->Top--;
return S->Data[S->Top];
}
}
5.栈的优缺点
6.
指向前一个位置栈满时为:top1-1=top2;
指向当前位置是top1+1=top2;
7.栈的应用
①括号匹配
#include <stdio.h>
#include <string.h>
#define TRUE 1
#define FALSE 0
int main()
{
int top=-1;
char str[105],stuck[55];
gets(str);
int i=0,flag=TRUE;
while(flag==TRUE && i<strlen(str))
{
switch ( str[i])
{
case'{':
case'[':
case'(':top++,stuck[top]=str[i];
break;
case')':if (stuck[top]!='(')
{
flag=FALSE;
break;
} else
top--;
break;
case']':if (stuck[top]!='[')
{
flag=FALSE;
break;
}else
top--;
break;
case'}':if (stuck[top]!='{')
{
flag=FALSE;
break;
}else top--;
break;
}
i++;
}
if (flag==TRUE&&top==-1)
{
printf("yes");
} else
printf("no");
}
②表达式求值
#include <stdio.h>
#include <string.h>
#define MAXSIZE 100
#define OK 1
#define OVERFLOW -1
typedef struct
{
char elem[MAXSIZE];
int top;
}SeqStack;
typedef struct
{
int elem[MAXSIZE];
int top;
}NUM;
void InitStack(SeqStack* s);
void InitStack1(NUM* s);
int EvalExpr(SeqStack* sign, NUM * num);
int pushsign(SeqStack* s, char sign);
int pushnum(NUM* s, int number);
void pushnewnum(NUM* s, int number);
char Precede(SeqStack* s, char ch);
int Operate(int a, char op, int b);
int main()
{
int sum;
SeqStack s[MAXSIZE],*sign=&s;
NUM n[MAXSIZE],*num=&n;
InitStack(sign);
InitStack1(num);
sum = EvalExpr(sign, num);
printf("%d",sum );
}
void InitStack(SeqStack* s)
{
s->top = -1;
}
void InitStack1(NUM * s)
{
s->top = -1;
}
int EvalExpr(SeqStack* sign, NUM * num)
{
char str[MAXSIZE];
gets(str);
for (int i = 0; i < strlen(str) + 1; i++)
{
char ch;
ch = str[i];
if (ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch == '(' || ch == ')' || ch == '^' || ch == '\0')
{
if (sign->top == -1)
{
pushsign(sign, ch);
}
else
{
switch (Precede(sign, ch))
{
case '<':pushsign(sign, ch);
break;
case '=':sign->top--;
break;
case '>':pushnewnum(num, Operate(num->elem[num->top], sign->elem[sign->top], num->elem[num->top - 1])),
sign->top--, i--; break;
}
}
}
else if (ch >= '0' && ch <= '9')
{
int e=ch-'0';
while (str[i+1]>= '0' && str[i+1]<= '9')
{
e=e*10+str[i+1]-'0';
i++;
}
pushnum(num, e);
}
}
return num->elem[0];
}
int pushsign(SeqStack* s, char sign)
{
if (s->top >= MAXSIZE - 1)
{
return OVERFLOW;
}
else
{
s->top++;
s->elem[s->top] = sign;
return OK;
}
}
int pushnum(NUM * s, int number)
{
if (s->top >= MAXSIZE - 1)
{
return OVERFLOW;
}
else
{
s->top++;
s->elem[s->top] = number;
return OK;
}
}
char Precede(SeqStack* s, char ch)
{
int i = 0, j = 0;
static char array[7][8] =
{
'>', '>', '<', '<', '<', '>', '<','>',
'>', '>', '<', '<', '<', '>', '<','>',
'>', '>', '>', '>', '<', '>', '<','>',
'>', '>', '>', '>', '<', '>', '<','>',
'<', '<', '<', '<', '<', '=', '<','!',
'>', '>', '>', '>', '!', '>', '!','>',
'>', '>', '>', '>', '<', '>', '!','>',
};
switch (ch)
{
//i为下面array的纵标
case '+': i = 0; break;
case '-': i = 1; break;
case '*': i = 2; break;
case '/': i = 3; break;
case '(': i = 4; break;
case ')': i = 5; break;
case '^': i = 6; break;
case '\0': i = 7; break;
}
switch (s->elem[s->top])
{ //j为下面array的横标
case '+': j = 0; break;
case '-': j = 1; break;
case '*': j = 2; break;
case '/': j = 3; break;
case '(': j = 4; break;
case ')': j = 5; break;
case '^': j = 6; break;
}
return (array[j][i]);
}
int Operate(int b, char op, int a)
{ //操作函数
int sum = 1;
switch (op) {
case '+': return (a + b);
case '-': return (a - b);
case '*': return (a * b);
case '/': return (a / b);
case'^':
for (int i = 0; i < b; i++)
{
sum = sum * a;
}
return sum;
}
return 0;
}
void pushnewnum(NUM * s, int number)
{
s->top--;
s->elem[s->top] = number;
}
3.3队列
1.队列的定义及特点
2.链队列的基本操作
顺序存储的缺点让我们选择另一种方式来作为队列——链式存储
先复习一下链表
①链队列结构定义
②首尾指针的创建
③链队列的初始化
④入队
int EnQueue(LinkQueue* Q, int e)
{
QueuePtr p;
p = (QNode*)malloc(sizeof(QNode));
p->data = e;
p->next = NULL;
Q->rear->next = p;
Q->rear = p;
return OK;
}
⑤出队
int DeQueue(LinkQueue* Q)
{
QueuePtr p;
if (Q->front == Q->rear)
{
return ERROR;
}
p = Q->front->next;
printf("%d", p->data);
Q->front->next = p->next;
if (Q->rear == p)
Q->rear = Q->front;
free(p);
return OK;
}
应用:银行叫号系统
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define OK 1
#define ERROR 0
#define maxsize 1000
typedef struct QNode {
int data;
struct QNode* front;
struct QNode* next;
} QNode, * QueuePtr;
typedef struct {
QueuePtr front;
QueuePtr rear;
} LinkQueue;
int EnQueue(LinkQueue* Q, int e);
int DeQueue(LinkQueue* Q,int *count);
void initstuck(LinkQueue* Q);
int main()
{
int n;
scanf("%d", &n);
int number[maxsize];
for (int j = 0; j < n; j++) {
scanf(" %d", &number[j]);
}
LinkQueue j[maxsize], * ji = j;
initstuck(ji);
LinkQueue o[maxsize], * ou = o;
initstuck(ou);
for (int i = 0; i < n; i++)
{
if (number[i] % 2 == 0)
{
EnQueue(ou, number[i]);
}
else
{
EnQueue(ji, number[i]);
}
}
int time=1;
for (int k = 0; k < n; k++)
{
int *count=NULL;
count=&time;
if (ji->front->next != NULL)
{
DeQueue(ji,count);
time++;
DeQueue(ji,count);
}
DeQueue(ou,count);
time++;
}
}
void initstuck(LinkQueue* Q)
{
Q->front = Q->rear = (QNode*)malloc(sizeof(QNode));
Q->front->next = NULL;
}
int EnQueue(LinkQueue* Q, int e)
{
QueuePtr p;
p = (QNode*)malloc(sizeof(QNode));
p->data = e;
p->next = NULL;
Q->rear->next = p;
Q->rear = p;
return OK;
}
int DeQueue(LinkQueue* Q,int *count)
{
QueuePtr p;
if (Q->front == Q->rear)
{
return ERROR;
}
p = Q->front->next;
if (*count==1)
{
printf("%d", p->data);
}else
printf(" %d", p->data);
Q->front->next = p->next;
if (Q->rear == p)
Q->rear = Q->front;
free(p);
return OK;
}
3.顺序队列
①顺序队列的初始化
②出队
③入队
④循环队列(解决假溢出)
何时为满何时为空?
解决二义性方法
循环队列出队
循环队列入队
3.3线性表异同点汇总
3.4栈和队列的一些题目
1-10循环队列是指用循环数组表示的队列。
1-11在用数组表示的循环队列中,front值可能大于可能小于rear值
1-12不采用牺牲一个空间的方式会产生队空与队满的二义性
2-6(x>0) ? x* f(x-1):2表示f(x)=xf(x-1),否则为2。则f(0)=2;f(1)=1f(0)=2;f(2)=2f(1)=21*f(0)=4
2-26在删除最后一个结点时,尾指针要移动
2-27出队时头指针要利用取模运算移动,因为数组大小为m+1,故为D
2-28因为要避免假溢出,所以要牺牲一个空间,尾指针指向当前最后一个元素的后一位,故为C
第四章 串、数组和广义表
4.1串的定义
串(String)----零个或多个字符组成的有限序列
串中任意连续的字符组成的子序列称为该串的字串,包含字串的串称为主串,某个字符在串中的序号称为这个字符的位置
4.2串的储存
顺序存储
用一组地址连续的存储单元存储串值的字符序列。该结构可用定长数组描 述如下:
#define MAXSTRLEN 255 //可在255以内定义最大串长
typedef unsigned char Sstring [MAXSTRLEN + 1]; // 0号单元存放串的长度
typedef struct
{ char *ch; //若串非空,则按串长分配存储区,
//否则ch为NULL
int length; //串长度
}HString;
链式存储
#define CHUNKSIZE 80 //可由用户定义的块大小 typedef struct Chunk{
char ch[CHUNKSIZE];
struct Chunk *next;
}Chunk;
typedef struct{
Chunk *head,*tail; //串的头指针和尾指针
int curlen; //串的当前长度
}LString;
4.3串的模式匹配算法
算法目的:
确定主串中所含子串第一次出现的位置 (定位) 即如何实现教材P72 Index(S,T,pos)函数
算法种类:
BF算法(又称古典的、经典的、朴素 的、穷举的)
KMP算法(特点:速度快)
int Index(Sstring S,Sstring T,int pos)
{ i=pos; 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;
}
BF算法时间复杂度
若n为主串长度,m为子串长度,最坏情况是
主串前面n-m个位置都部分匹配到子串的最后一位, 即这n-m位各比较了m次
最后m位也各比较了1次
总次数为:(n-m)*m+m=(n-m+1)m
若m<<n,则算法复杂度O(nm)
4.4数组
一维数组
二维数组
三维数组
n维数组
4.5特殊矩阵的压缩
1.什么是压缩存储? 若多个数据元素的值都相同,则只分配一个元素值 的存储空间,且零元素不占存储空间。
2.什么样的矩阵能够压缩? 一些特殊矩阵,如:对称矩阵,对角矩阵,三角矩 阵,稀疏矩阵等。
3.什么叫稀疏矩阵? 矩阵中非零元素的个数较少(一般小于5%)
特殊矩阵
4.6广义表
广义表是线性表的推广,也称为列表。广泛应用于人工智能等领域的表处理语言LISP语言,把广义表作为基本的数据结构,就连程序也表示为一系列的广义表。
广义表一般记作LS=(a1,a2,……,an)
其中LS是广义表(a1,a2,……,an)的名字,n是长度。在广义表的定义中,ai既可以是单个元素,也可以是广义表,分别称为广义表LS的原子和子表
(1)A=()——4是一个空表,其长度为零。
(2) B= (e)——B只有一一个原子e,其长度为1。
(3)C=(a,(b,c,d) )——C 的长度为2,两个元素分别为原子a和子表(b,c,d)。
(4)D=(A,B,C) ——D 的长度为3, 3个元素都是广义表。显然,将子表的值代入后,则有D=((),(e),(a, (b, c, d)))。
(5)E=(a,E)——这是一个递归的表, 其长度为2。E相当于一个无限的广义表E=(a, (a,(a, ……)))。
由于广义表的结构比较复杂,其各种运算的实现也不如线性表简单,其中,最重要的两个运算如下。
(1)取表头GetHead(LS):取出的表头为非空广义表的第一一个元素, 它可以是一一个单原子,也
可以是一一个子表。
(2)取表尾GetTail(LS):取出的表尾为除去表头之外,由其余元素构成的表。即表尾- -定是
一个广义表。
例如:
GetHead(B)=e,
GetTail(B)=( ),
GetHead(D)= A,
GetTail(D)=(B,C),
由于(B,C)为非空广义表,则可继续分解得到:
GetHead(B, C)= B, GetTail(B, C)= ©,
值得提醒的是,广义表( )和(( ))不同。前者为空表,长度n=0;后者长度n=1,可分解得到其表头、表尾均为空表( )。
4.7题目
2-5答案正确,串的子串应该为n*(n+1)*0.5+1(空串)=37
第五章 树和二叉树
5.1树的定义和基本术语
1.树的定义:树是由n (n >= 0)个结点组成的有限集合T。 如果n = 0,称为空树; 如果n > 0,则T满足以下两个条件: ❖ 有且只有一个特定的称之为根的结点;
❖ 其它结点划分为m (m >= 0) 个互不相交的有限集合 T1, T2, …, Tm,
其中每个集合又是一棵树, 并且称之为根的子树。
2.树的特点
▪ 根结点可以有0个或多个直接后继(其每棵 子树的根结点),但没有直接前驱。
▪ 除根以外的其它结点 有且仅有一个直接前驱, 但可以有0个或多个 直接后继。
3.树的基本术语
**(1)结点:**树中的一个独立单元。包含一个数据元素及若干指向其子树的分支,如图中的A、B、C、D等。(下面术语中均以图为例来说明)
**(2)结点的度: **结点拥有的子树数称为结点的度。例如,A的度为3,C的度为1,F的度为0。
**(3)树的度:**树的度是树内各结点度的最大值。图所示的树的度为3。
**(4)叶子:**度为0的结点称为叶子或终端结点。结点K、L、F、G、M、I、J都是树的叶子。
**(5)非终端结点:**度不为0的结点称为非终端结点或分支结点。除根结点之外,非终端结点也称为内部结点。
**(6)双亲和孩子:**结点的子树的根称为该结点的孩子,相应地,该结点称为孩子的双亲。例如,B的双亲为A,B的孩子有E和F。
**(7)兄弟:**同一个双亲的孩子之间互称兄弟。例如,H、I和J互为兄弟。
**(8)祖先:**从根到该结点所经分支上的所有结点。例如,M的祖先为A、D和H。
**(9)子孙:**以某结点为根的子树中的任-结点都称为该结点的子孙。如B的子孙为E、K、L和F。
**(10)层次:**结点的层次从根开始定义起,根为第一层,根的孩子为第二层。树中任一结点的层次等于其双亲结点的层次加1。
**(11)堂兄弟:**双亲在同一层的结点互为堂兄弟。例如,结点G与E、F、H、I、J互为堂兄弟。
**(12)树的深度:**树中结点的最大层次称为树的深度或高度。图所示的树的深度为4。
**(13)有序树和无序树:**如果将树中结点的各子树看成从左至右是有次序的(即不能互换),则称该树为有序树,否则称为无序树。在有序树中最左边的子树的根称为第一个孩子,最右边的称为最后一个孩子。
**(14)森林:**是m(m≥0)棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。由此,也可以用森林和树相互递归的定义来描述树。
就逻辑结构而言,任何一棵树都是 一个二元组Tree= (root, F),其中root是数据元素,称作树的根结点; F是m(m≥0)棵树的森林,P=(T1,T2, ……Tm),其中T=(ri,Fi)称作根r的第i棵子树;当m≠0时,在树根和其子树森林之间存在下列关系:RF={<root,r>|i=1,2,4 ,m,m>0}
这个定义将有助于得到森林和树与二叉树之间转换的递归定义。
5.2二叉树
1.二叉树的特点是每个结点至多只有两棵子树(即二叉树 中不存在度大于2的结点),并且,二叉树的子树有左右之分,其次序不能任意颠倒。
五种基本状态
2.性质
性质1:若二叉树的层次从1开始, 则在二叉树 的第 i 层最多有 2i-1 个结点(i >= 1)
性质2:高度为k的二叉树最多有 2k-1个结点 (k >= 1)
20 + 21 + 22 + 23 + … + 2k-1 = 2k - 1
性质3:对任何一棵二叉树, 如果其叶结点个数为n0, 度为2的非叶结点个数为 n2, 则有n0=n2+1
完全二叉树:每个结点都与等高的满二叉树中编号 一一对应的二叉树。 若一棵二叉树只有最下面两层结点的度可以小于2;并且最下层的结点都集中在该层最左边的若干连续的位置上, 则该二叉树称作完全二叉树。
性质4:具有n个结点的完全二叉树的高度为 (log2 n)的下取整 +1 。
性质5:若一棵有n个结点的完全二叉树的结点按层序编 号,则对结点i (1 <= i <= n) ,有以下关系:
性质6:◼ 若2i <= n,则i 的左孩子为2 i ;否则i (必为叶结点) 无左孩子
◼ 若2i +1<= n ,则i 的右孩子为2 i +1 ;否则i无右孩子。
3.二叉树的存储结构
①• 二叉树的顺序存储表示
•用一组连续的存储单元存储二叉树的数据元素,
• 必须把二叉树的所有结点安排成为一个恰当的序列,结点在这个序列中的相互位置能反映出结点 之间的逻辑关系。 ——用一组地址连续的存储单元存储完全二叉树的数据元素。其中编号为 i 的结点元素存放在一维数组的下标为 i-1 的分量中。
#define MAX_TREE_SIZE 100 // 二叉树的最大结点数
typedef TElemType SqBiTree[MAX_TREE_SIZE]; // 0号单元存储根结点 SqBiTree bt;
②链式存储结构
1)二叉链表二叉树的链表中的结点至少包含三个域: 数据域、左指针域、右指针域:lchild data rchild
typedef struct BiTNode { // 结点结构
TElemType data;
struct BiTNode *lchild, *rchild; //左右孩子指针
} BiTNode, *BiTree;
2)三叉链表
为了便于找到结点的双亲,则可在二叉链表的结点结 构中增加一个指向双亲的指针域。
5.3遍历二叉树和线索二叉树
1.遍历二叉树
二叉树的遍历方法
令 L:遍历左子树 D:访问根结点 R:遍历右子树
D L R 先序遍历——L D R 中序遍历——L R D 后序遍历
先序遍历二叉树
Status Preorder (BiTree T)
{ // 先序遍历二叉树
if (T) {
printf(T->data); // 访问结点
Preorder(T->lchild); // 遍历左子树
Preorder(T->rchild);// 遍历右子树
}
}
其余遍历方法只需交换三者顺序
2.二叉树的遍历应用举例
统计出给定二叉树中结点的数目
int count(BiTree T)
{
int num1
int num2
if (T==NULL) num=0;
else {
num1=count(T->lchild);
num2=count(T->rchild);
num= num1+num2+1;
}
return num;
}
二叉树的建立(按先序序列建立二叉树)
Status CreateBiTree(BiTree &T)
{
scanf(&ch);
if (ch==‘#‘) T=NULL;
else
{
if(!(T=(BiTree)malloc(sizeof(BiNode))))
exit OVERFLOW;
T->data=ch;
CreateBiTree(T->lchild);
CreateBiTree(T->rchild);
}
return OK;
}//CreateBiTree
建立如上二叉树的二叉链表结构应输入:ABC##DE#G##F###
3.线索二叉树
分析与设计
一个结论: • 用二叉链表法存储包含n个结点的二叉树,结点的 指针域中会有n+1个空链域。
思考: • 二叉链表空间效率这么低,能否利用这些空闲区 存放有用的信息或线索?
解决方案: • 我们可以用它来存放当前结点的直接前驱和直接 后继等线索,以加快查找速度。
typedef enum {Link,Thread} PointerTag;
//Link==0 :指针,指向孩子结点
//Thread==1 :线索,指向前驱或后继结点
typedef struct BiThrNode{
ElemType data;
struct BiThrNode *lchild,*rchild;
PointerTag ltag,rtag;
}BiThrNode,*BiThrTree
5.4树和森林
一、树的三种存储结构
1)、双亲表示法 ( 顺序存储结构 )
利用每个非根结点有一个双亲的性质,在每个结点 附设指示器指示其双亲所在位置。
2)、孩子链表表示法
1、多重链表表示法
每个结点有多个指针域,其中每个指针指向一棵子树根结点。结点有两种方式: a) 链表中结点同构,链表中域的数目为树的度
d为树的度
data | child1 | child2 | …… | childd |
---|
▪若结点采用格式a)表示,则空间可能会较浪费;
b)非固定大小结点结构
d为结点的度
data | degree | child1 | child2 | …… | childd |
---|
▪若结点采用格式b)表示,则操作较为不便。
2、孩子链表表示法:把树的每个结点的孩子排列起来, 看成一个线性表,且以单链表作存储结构。则n个结点的 树就有n个孩子链表;并将n个头指针也看成一个线性表, 采用顺序存储结构。
孩子链表便于涉及孩子的操作的实现,却不适用于涉及双亲 的操作,可将其和双亲表示结合在一起。
3、二叉链表(孩子-兄弟)表示法
链表中结点的两个链域分别指向该结点的第一个 孩子结点和下一个兄弟结点。
结点结构 :
firstchild | data | nextsibling |
---|
typedef struct CSNode{
Elem data;
struct CSNode *firstchild, *nextsibling;
} CSNode, *CSTree;
此二叉链表既是树的孩子兄弟表示又是二叉树的二叉链表表示
二、森林与二叉树的转换
a)树转化为二叉树 以二叉链表为媒介可导出树与二叉树之间的一个对应关 系。用一棵二叉树表示一棵树,可以很好地解决树的存储表 示问题,而且树的各种操作均可对应二叉树的操作来完成。
转换方法: 1、(连兄弟)在树中各兄弟之间加一连线;
2、(断父子)对于任一结点,只保留它与最左孩 子之间的连线;
3、(转一转)以树的根结点为轴心,将整棵树按 顺时针方向旋转成二叉树。
说明: ▪由树转换成的二叉树,其根结点的右子树总是空的。
▪二叉树的根结点即是对应树的根结点,二叉树中任何结 点与其左孩子是树中双亲孩子关系,而与其右孩子则是 对应树中的兄弟关系。
b)二叉树转化为树
转换方法: 1、(连祖孙) 将结点与其左孩子的右子孙连接;
2、(断父子) 对于任一结点,只保留它与最左孩子 之间的连线;
3、(抖一抖) 使任一结点的子树无左右之分。
c)、森林转化为二叉树
转换方法1: ▪将森林中的每一棵树依次转换成相应的二叉树;
▪将第二棵作为第一棵二叉树的根结点的右子树 连接起来,将第三棵又作为第二棵的右子树连接 起来…直至把所有的二叉树连接成一棵二叉树
转换方法2:▪增添一个虚拟的“根结点”,使森林中的 每一棵树依次成其子树。▪将该树转化为二叉树。 ▪删除增添的 “根结点”
d)二叉树转化为森林
转换方法1:
▪删去连接根与右孩子的分枝;若右子树的右子树二叉树 不空,则继续删,直至右子树的右子树二叉树为空;
▪将森林中的每一棵二叉树依次转换成树。
转换方法2
▪ 增添一个虚拟的“根结点”,使该二叉 树成为一棵右子树为空的二叉树;
▪ 将该二叉树转化为树。
▪ 删除该树的根结点 。
5.5哈夫曼树及其应用
电报编码
任一字符的编码不是其他字符编码的前缀,称为前缀编码
如何能让编码总长度最短且译码唯一?——最优二叉树
• 结点的路径长度l ,从根结点到该结点的 路径上分支的数目
• 树的路径长度 , 树中所有叶子结点的 路径长度之和
结点的权 w
结点的带权路径长度 l*w
树的带权路径长度WPL= 每个li * wi的和
最优二叉树树的特点
➢ 权值小的结点离根远 , 权值大的结点离根近;
➢ 结点的度:没有度为 1 的 结点。
哈夫曼树的构建
#define max 105
#define MAXLEAF 6 /*定义哈夫曼树中叶子结点的个数*/
#define MAXNODE MAXLEAF*2-1//n个叶子节点的哈夫曼有2n-1个结点
typedef struct {
int weight;//权重
int parent, lchild, rchild;//双亲,左、右孩子
}htnode;
void creathuffmantree(int n, int weight[])
{
if (n < 1) return;
for (int i = 0; i < MAXNODE; i++)
{
tree[i].parent = -1;
tree[i].lchild = -1;
tree[i].rchild = -1;
}
for (int j = 0; j < n; j++) {
tree[j].weight = weight[j];//权值输入
}
//---------------------------------------------------初始化
for (int k = MAXLEAF; k < MAXNODE; k++)
{
int m1 = max, m2 = max, s1=0, s2=0;
for (int i = 0; i < k; i++)//选择两个最小的权值,且双亲域为0
{
if (tree[i].weight < m1 && tree[i].parent == -1)
{
m2 = m1; s2 = s1;
m1 = tree[i].weight;
s1 = i;
}//m1最小,s1为其位置
else if (tree[i].weight < m2 && tree[i].parent == -1)
{
m2 = tree[i].weight;
s2 = i;
}//m2次小,s2为其位置
}
tree[s1].parent = k;
tree[s2].parent = k;//得到新结点
tree[k].lchild = s1;
tree[k].rchild = s2;//新结点孩子
tree[k].weight = tree[s1].weight + tree[s2].weight;//新结点权重
}
}
哈夫曼编码的实现
#define max 105
#define MAXLEAF 6 /*定义哈夫曼树中叶子结点的个数*/
#define MAXNODE MAXLEAF*2-1//n个叶子节点的哈夫曼有2n-1个结点
typedef struct Code
{
int bits[MAXLEAF];
int start;
char ch;
}HCodeType;/*定义编码结构*/
void Huffmancode()
{
int c, p, i, j;
for (i = 0; i < MAXLEAF; i++)
{
HuffCode[i].start = MAXLEAF;
printf("请输入字符:");
scanf("%c",&HuffCode[i].ch);
getchar();
c = i;
p = tree[i].parent;
while (p != -1)//到根节点
{
HuffCode[i].start--;//从数组后面往前记录
if (tree[p].lchild == c)
{
HuffCode[i].bits[HuffCode[i].start] = 0;//c是p的左孩子为0
}
else
{
HuffCode[i].bits[HuffCode[i].start] = 1;//右孩子为1
}
c = p;//c向上走
p = tree[p].parent;//p向上走
}
}
for (i = 0; i < MAXLEAF; i++)/*输出字符、权值及编码*/
{
printf("字符 %c 权值 %d,编码是:", HuffCode[i].ch, tree[i].weight);
for (j = HuffCode[i].start; j < MAXLEAF; j++)
printf(" %d ", HuffCode[i].bits[j]);
printf("\n");
}
}
5.6题目
1-14是错误的,应当是哈夫曼树是带权路径长度最短的二叉树,路径上权值较大的结点离根较近。
必须是完全二叉树才能有
第六章 图
6.1认识图
一、图的定义
图是一种多对多的关系,每个元素可以有零 个或多个直接前趋;零个或多个直接后继。
图G由两个集合构成G= (V, {A})
V是顶点的非空有限集合
A 是边的有限集合(边是顶点的无序对或有序对)->无向图或有向图
二、图的基本术语
子图
邻接点
度、入度、出度
路径与回路
简单路径和简单回路
连通图、连通分量、强连通图、强连通分量
连通图:在无向图G=(V,{E})中,若从vi到vj有路径相通, 则称顶点vi与vj是连通的。如果对于图中的任意两个顶点vi、 vj∈V,vi,vj都是连通的,则称该无向图G为连通图。
连通分量:无向图中的极大连通子图称为该无向图的连通分量。
强连通图:在有向图G=(V,{A})中,若对于每对顶点vi、vj∈V且vi≠vj,从vi到vj和vj到vi都有路径,则称该有向图 为强连通图。
强连通分量:有向图的极大强连通子图称作有向图的强连通 分量。
6.2图的存储结构
图的存储结构方法有:
①邻接矩阵表示法;②邻接表;③邻接多重表;④十字链表。主要利用前两个
关键问题:顶点的存储、边的存储
1.邻接矩阵表示法
图的邻接矩阵表示法也称作数组表示法。 设置一个二维数组,用于存储图中顶点之间关联关系,这个关联关系数组被称为邻接矩阵。
若G是一具有 n 个顶点的有向图,G的邻接矩阵是具有 如下性质的n×n矩阵A:
A[i,j]=
1 | 若<vi,vj>或(vi,vj)属于VR |
---|---|
0 | 反之 |
对应行上的1的个数为其出度,对应列上为入度
typedef enum{DG,DN,UDG,UDN} GraphKind; //{有向图,有向网,无向图,无向网}
typedef struct { //图的定义
VertexType vexs[MAX_VERTEX_NUM]; //顶点向量—保存顶点数据 i
nt arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; //邻接矩阵—保存顶点间关系
int vexnum, arcnum; //顶点数,弧数
GraphKind kind; //图的种类标志
} MGraph;
无向图数组表示法特点: – 无向图邻接矩阵是对称矩阵;
– 判断两顶点v、u是否为邻接点-二维数组对应分量是否非零;
– 顶点不变,在图中增加、删除边
– 对二维数 组对应分量赋非零值或清零;
– 二维数组存储顶点数为 n的图, 占用存储空间 只与它的顶点数n有关,与边数e无关。适合边稠密的图
2.邻接表
顶点的结点结构
data | firstarc |
---|
data firstarc
typedef struct VNode {
VertexType data; // 顶点信息
ArcNode *firstarc; // 指向第一条依附自该顶点的弧
} VNode, AdjList[MAX_VERTEX_NUM]; // 头结点、头结点数组类型.
弧的结点结构
adjvex | nextarc |
---|
typedef struct ArcNode {
int adjvex; // 该弧所指向的顶点的位置
struct ArcNode *nextarc; // 指向下一条弧的指针
} ArcNode; // 表结点
3.比较
6.3图的遍历
1.深度优先遍历(纵深)
深度优先搜索(Depth_First Search)是指按照深度 方向搜索 。 深度优先算法的基本思想是:
(1)从图中某个顶点v0出发,首先访问v0。
(2)找出刚访问过的顶点vi的第一个未被访问的邻 接点,然后访问该顶点。重复此步骤,直到当前的 顶点没有未被访问的邻接点为止。
(3)返回前一个访问过的顶点,找出该顶点的下一 个未被访问的邻接点,访问该顶点。转2。
void DFSTraverse(Graph G) {// 对图 G 作深度优先遍历
for (v=0; v<G.vexnum; ++v)
visited[v] = FALSE; // 访问标志数组初始化
for (v=0; v<G.vexnum; ++v)
if (!visited[v])
DFS(G, v); //若为连通图一次调用DFS即可
// 对尚未访问的顶点调用DFS
}
void DFS(Graph G, int v) { // 从顶点v出发,深度优先搜索遍历连通图 G
visit(v);//访问顶点V
visited[v] = TRUE; //标志数组记录是否已访问
w = FirstAdjVex(G,v);
while(w>=0)
{
if(visited[w]==FLASE) DFS(G,W);
else w=NextAdjVex(G,v,w);
}
} // DFS
2.广度优先搜索
广度优先搜索(Breadth_First Search)是指按照 广度方向搜索。
广度优先搜索的基本思想是:
(1)从图中某个顶点v0出发,首先访问v0。
(2)依次访问v0的各个未被访问的邻接点。
(3)分别从这些邻接点(端结点)出发,依次访 问它们的各个未被访问的邻接点(新的端结点)
说明:在广度优先遍历图时, “先被访问的顶点的邻接点” 先于“后被访问的顶点的邻接 点”被访问。—即以V为起始点, 由近至远,依次访问和V有路径相通且路径长度为1,2,…的顶点
oid BFSTraverse(Graph G, Status (*Visit)(int v)){
for (v=0; v<G.vexnum; ++v)
visited[v] = FALSE; //初始化访问标志
InitQueue(Q); // 置空的辅助队列Q
for ( v=0; v<G.vexnum; ++v )
if ( !visited[v]) {// v 尚未访问
visited[v] = TRUE;
Visit(v); EnQueue(Q, v);// v入队列
while (!QueueEmpty(Q))
{
DeQueue(Q, u); // 队头元素出队并置为u
for(w=FirstAdjVex(G, u); w>=0; w=NextAdjVex(G,u,w))
if ( ! visited[w])
{
visited[w]=TRUE; Visit(w);
EnQueue(Q, w);
}
}
}
} // BFSTraverse
(1)、顶点何时入队?何时出队? 被访问时入队,当前链表访问到末尾时出队
(2)、广度优先搜索何时结束? 队列为空
3.最小最小生成树:选择原图中权值之和最小的N-1条边,使得N个顶点刚好连通
普里姆(Prim)算法
【算法步骤】
①首先将初始顶点u加入U中,对其余的每一个顶点vj,将closedge均初始化为到u的边信息。
②循环n-1次,做如下处理:
●从各组边 closedge中选出最小边closedge[k],输出此边;
● 将k加入U中;
●更新剩余的每组最小边信息closedge[j], 对于V-U中的边,新增加了一条从k到j的边,如果新边的权值比closedge[j].lowcost小,则将closedge[j].lowcost更新为新边的权值。
void MiniSpanTree. Prim (AMGraphG, VerTexType u)
{//无向网G以邻接矩阵形式存储,从顶点u出发构造G的最小生成树T,输出T的各条边
k=LocateVex (G,u);//k为顶点u的下标
for (j=0;j<G.vexnum;++j)//对V-U的每一个顶点Vj,初始化closedge[j]
if(j!=k) closedge[j]={u, .arcs[k][j]}; //{adjvex lowcost}
closedge[k] . lowcost=0;//初始,U={u}
for (i=1; i <G. vexnum; ++i)
{//选择其余n-1个顶点,生成n-1条边(n=G. vexnum)
k=Min (closedge) ;
//求出T的下一个结点:第k个顶点,closedge [k]中存有当前最小边
u0=closedge [k] .adjvex; //u0为最小边的一个顶点,u0∈U
v0=G.vexs[k]; //v0为最小边的另一个顶点,v0∈V-U
cout<<u0<<v0; //输出当前的最小边(u0, v0)
closedge[k].lowcost=0;//第k个顶点并入U集
for (j=0;j<G. vexnum;++j)
if(G. arcs[k] [j]<closedge[j]. lowcost) //新顶点并人U后重新选择最小边
closedge[j]={G.vexs[k],G.arcs[k][j];
}
}
4.克鲁斯卡尔(kruskal)算法
假设N={V,{VR}}是一个连通图,则令最小生成树初始状 态只有n个顶点而无边的非连通图T={V,{}},图中每个顶 点自成一个连通分量。克鲁斯卡尔算法执行如下操作:
在VR中选择代价最小的边,若该边依附的顶点落在T 中不同的连通分量上,则将此边加入到T中;否则舍去此 边而选择下一条代价最小的边。依次类推,直至T中所有 顶点都在同一分量上为止,此时的T即为最小生成树。
算法6.9克鲁斯卡尔算法
[算法步骤]
①将数组Edge中的元素按权值从小到大排序。
②依次查看数组Edge中的边,循环执行以下操作:
●依次从排好序的数组Edge中选出一条边(U1,U2):
●在Vexset中分别查找V1和以所在的连通分量V85和VS2,进行判断:
➢如果VS1和VS2不等,表明所选的两个顶点分属不同的连通分量,输出此边,并合并VSI和VS2两个连通分量;
➢如果VS1 和VS2相等,表明所选的两个顶点属于同一个连通分量,舍去此边而选择下一条权值最小的边。
void MiniSpanTree_ Kruskal (AMGraph G)
{//无向网G以邻接矩阵形式存储,构造G的最小生成树了,输出T的各条边
Sort (Edge) ; //将数组Edge中的元素按权值从小到大排序
for(i=0; i<G. vexnum; ++i) //辅助数组,表示各顶点自成一个连通分量
Vexset[i]=i;
for(i=0; i<G. arcnum;++i) //依次查看数组Edge中的边
{
v1=LocateVex (G, Edge [i] .Head) ; //v1为边的始点Head的下标
v2=LocateVex (G, Edge[i] .Tail); //v2为边的终点Tail的下标
vs1=Vexset [v1] ;//获取边Edge[i]的始点所在的连通分量vs1
vs2=Vexset [v2] ;//获取边Edge [i]的终点所在的连通分量vs2
if(vs1!=vs2)//边的两个顶点分属不同的连通分量
{
cout<< Edge[i].Head<< Edge[i].Tail //输出此边
for(j=0;j<G. vexnum;++j)//合并vS1和VS2两个分量,即两个集合统一2编号
if (Vexsetlij==vs2) Vexset[j)=vs1; //集合编号为vs2的都改为vs1
}
}
}
5. Dijkstra算法– 引入辅助数组D[ ]存储当前所找到的从开始点 V到终点Vi的最短路径的长度 – 集合S为最短路径的顶点集合
6. 弗洛伊德floyd
详见人民邮电出版社严蔚敏数据结构c语言第二版172页-175页
7.AOV网&拓扑排序
拓扑序列:• 如果图中从V到W有一条有向路径,则V一定排在 W之前,满足此条件的顶点序列称为一个拓扑序 列。
void TopSort()
{
for (图中每个顶点 V )
if ( Indegree[V]==0 )
Push( S, V );
while ( !IsEmpty(S) )
{
Pop(S,V);
输出V ; cnt++;
for ( V 点 的每个邻接点 W )
if ( --Indegree[W]==0 )
Push( S, V );
}
If( cnt<n)
Error( “ 图中有回路” );
8.AOE网&关键路径
• AOE(Activity On Edge)网
– 带权的有向无环图
– 顶点表示事件,弧表示活动,权表示活动持 续的时间
在AOE网上完成工程的最短时间是从开始 点到完成点的最长路径的长度
关键路径上的所有活动都是关键活动
6.4题目
拓扑排序中不能有环,邻接表中的每个结点后的个数与边有关
上述三个题目答案有误,均应为正确的
深度遍历类似树的先序遍历,广度类似层次
起始结点不同树不同
第七章 查找与排序
7.1查找
1.顺序查找
2.折半查找
3.分块查找
4.树表的查找
二叉排序树
中序遍历二叉排序树:得到一个关键字递增的有序序列
查找
平均时间复杂度O(log2n)
插入
删除
5.散列查找(哈希查找)
除留余数法构建哈希函数会有冲突,于是有
①开放地址法
线性探测法(左右依次位置)、二次探测法(左右依次1、4、9、n2)、伪随机探测法(+随机数)
②链地址法
装填因子
7.2排序
1.插入排序
直接插入排序:每次找数据的合适位置插入,有元素的移动
希尔排序的核心还是直接插入排序,两两比较多次,每次间隔变小,最终间隔为1
2.交换排序
①冒泡排序
②快速排序
3.选择排序
①简单选择排序
②树形选择排序
③堆排序
基本思想:
5.归并排序
6.基数排序
最高位优先法
最低位优先法
链式基数排序
算法分析
排序算法总分析
7.3题目
归>快>堆
关键路径上的活动最早与最晚相同
在散列表中,所谓同义词就是:具有相同散列地址的两个元素
未完待续