线性表
单链表
线性表的单链表存储结构
/* 线性表的单链表存储结构 */
typedef struct Node
{
ElemType data;
struct Node *next;
} Node;
typedef struct Node *LinkList;
获得链表第i个数据
- 声明一个指针p指向链表第一个结点,初始化j=1;
- 当j<i时,就遍历链表,让p不断指向下一结点,j累加1;
- 若到链表末尾p为空,则说明第i个结点不存在;
单链表第i个数据插入结点
- 声明指针p指向链表头结点,初始化j=1;
- 当j<i时,就遍历链表,让p指针不断指向下一结点,j累加1;
- 若到链表末尾p为空,则说明第i个结点不存在;
- 否则查找成功,在系统中生成一个空结点s;
- 将数据元素e赋值给s->data;
- 单链表的插入标准语句s->next=p->next;p->next=s;
单链表第i个数据删除结点
- 声明一指针p指向链表头结点,初始化j=1;
- 当j<i时,就遍历链表,让p指针不断指向下一个结点,j累加1;
- 若到链表末尾p为空,则说明第i个结点不存在;
- 否则查找成功,将欲删除的结点p->next赋值给q;
- 单链表的删除标准语句p->next=q->next;
- 将q结点中的数据赋值给e,作为返回;
- 释放q结点;
单链表的整表创建
头插法
- 声明一指针p和计数器变量i;
- 初始化一空链表L;
- 让L的头结点的指针指向NULL,即建立一个带头结点的单链表;
- 循环
{
生成一新结点赋值给p;
随机生成一数字赋值给p的数据域p->data;
将p插入到头结点与前一新结点之间。
}
尾插法反过来即可
单链表的整表删除
- 声明一指针p和q;
- 将第一个结点赋值给p;
- 循环:
{
将下一结点赋值给q;
释放p;
将q赋值给p。
}
这里常见的错误就是觉得q变量没有存在的必要。在循环体内直接写free(p); p=p->next;
即可。
要知道p指向一个结点,它除了有数据域,还有指针域。你在做free(p);
时,其实是在对它整个结点进行删除和内存释放的工作。变量q的作用,它使得下一个结点是谁得到了记录,以便于等当前结点释放后,把下一结点拿回来补充。
链表中第一个结点的存储位置叫做头指针
头指针就是链表的名字。头指针仅仅是个指针而已。如果删除链表中的节点,头指针不会改变(头指针不能删除。
销毁与清空有别:
销毁:
销毁的时候,是先销毁链表的头,然后接着把后面的销毁,这样这个链表就不能再使用了
Status DestroyList(LinkList *L)
{
LinkList q;
while(*L)
{
q=(*L)->next;
free(*L);
*L=q;
}
}
清空:
清空的时候,是先保留链表的头,然后把后面所有都销毁,最后把头指向下一个的指针设为空,这样就相当与清空了,但这个链表还在,还可以继续使用
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;
}
单链表完整演示代码:
#include<stdio.h>
#include<time.h>
#include<stdlib.h>
#include<windows.h>
void visit(int c)
{
printf("%d ",c);
return;
}
typedef struct Node
{
int data;
struct Node* next;
}Node;
typedef struct Node* LinkList;
/* 初始化顺序线性表 */
int InitList(LinkList* LLL) /*传入头结点指针的指针(因为要修改头结点的指针,所以要传入指针的指针)*/
{
*LLL=(LinkList)malloc(sizeof(Node)); /* 为头结点所在的区域分配内存,产生头结点(*LLL=*L,LLL=**L) */
if(!(*LLL)) /* 存储分配失败 */
return 0;
(*LLL)->next=NULL; /* 头结点指针域为空 */
return 1;
}
/* 判空 */
int ListEmpty(LinkList L) /*传入头结点的指针,即头指针*/
{
if(L->next)
return 0;
else
return 1;
}
/* 清空表(销毁与清空有别)*/
int ClearList(LinkList* L) //改变头指针的函数要传入头指针的指针来改变 不修改头指针的函数可传入头指针
{
LinkList p,q;
p=(*L)->next; /* p指向第一个结点 */
while(p) /* 没到表尾 */
{
q=p->next;
free(p);
p=q;
}
(*L)->next=NULL; /* 头结点指针域为空 */
return 1;
}
/* 返回L中数据元素个数 */
int ListLength(LinkList L)
{
int i=0;
LinkList p=L->next; /* p指向第一个结点 */
while(p)
{
i++;
p=p->next;
}
return i;
}
/* 用e返回L中第i个数据元素的值 */
int GetElem(LinkList L,int i,int* e)
{
int j;
LinkList 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 0; /* 第i个元素不存在 */
*e = p->data; /* 取第i个元素的数据 */
return 1;
}
/* 返回L中第1个等于e的数据的位序 */
int LocateElem(LinkList L,int e)
{
int i=0;
LinkList p=L->next;
while(p)
{
i++;
if(p->data==e)
return i;
p=p->next;
}
return 0;
}
/* 在L中第i个位置之前插入新的数据元素e,L的长度加1 */
int ListInsert(LinkList* L,int i,int e)
{
int j=1;
LinkList p,s;
p = *L;
while (p && j < i) /* 寻找第i个结点 */
{
p = p->next;
++j;
}
if (!p || j > i)
return 0; /* 第i个元素不存在 */
s = (LinkList)malloc(sizeof(Node)); /* 生成新结点 */
s->data = e;
s->next = p->next; /* 将p的后继结点赋值给s的后继 */
p->next = s; /* 将s赋值给p的后继 */
return 1;
}
/* 删除L的第i个数据元素,并用e返回其值,L的长度减1 */
int ListDelete(LinkList* L,int i,int* 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 0; /* 第i个元素不存在 */
q = p->next;
p->next = q->next; /* 将q的后继赋值给p的后继 */
*e = q->data; /* 将q结点中的数据给e */
free(q); /* 让系统回收此结点,释放内存 */
return 1;
}
/* 依次对L的每个数据元素输出 */
int ListTraverse(LinkList L)
{
LinkList p=L->next;
while(p)
{
visit(p->data);
p=p->next;
}
printf("\n");
return 1;
}
/* 随机产生n个元素的值,建立带表头结点的单链线性表L(头插法) */
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; /* 插入到表头 */
}
}
/* 随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法) */
void CreateListTail(LinkList* L, int n)
{
LinkList p,r;
int i;
srand(time(0)); /* 初始化随机数种子 */
*L = (LinkList)malloc(sizeof(Node)); /* L为整个线性表 */
r=*L; /* r为指向尾部的结点 */
for (i=0; i<n; i++)
{
p = (Node *)malloc(sizeof(Node)); /* 生成新结点 */
p->data = rand()%100+1; /* 随机生成100以内的数字 */
r->next=p; /* 将表尾终端结点的指针指向新结点 */
r = p; /* 将当前的新结点定义为表尾终端结点 */
}
r->next = NULL; /* 表示当前链表结束 */
}
int main()
{
LinkList L; // L为头结点第一项的地址。L->next为头结点的指针域
int e,i,j,k;
InitList(&L);
printf("初始化L后:ListLength(L)=%d\n",ListLength(L));
for(j=1;j<=5;j++)
i=ListInsert(&L,1,j);
printf("在L的表头依次插入1~5后:L.data=");
ListTraverse(L);
printf("ListLength(L)=%d \n",ListLength(L));
ClearList(&L);
for(j=1;j<=10;j++)
ListInsert(&L,j,j);
printf("在L的表尾依次插入1~10后:L.data=");
ListTraverse(L);
printf("ListLength(L)=%d \n",ListLength(L));
ListInsert(&L,1,0);
printf("在L的表头插入0后:L.data=");
ListTraverse(L);
printf("ListLength(L)=%d \n",ListLength(L));
GetElem(L,5,&e);
printf("第5个元素的值为:%d\n",e);
k=ListLength(L);
for(j=k+1;j>=k;j--)
{
i=ListDelete(&L,j,&e); /* 删除第j个数据 */
if(i==0)
printf("删除第%d个数据失败\n",j);
else
printf("删除第%d个的元素值为:%d\n",j,e);
}
ClearList(&L);
CreateListHead(&L,20);
printf("整体创建L的元素(头插法):");
ListTraverse(L);
ClearList(&L);
CreateListTail(&L,20);
printf("整体创建L的元素(尾插法):");
ListTraverse(L);
system("pause");
return 0;
}
静态链表
/* 线性表的静态链表存储结构 */
#define MAXSIZE 1000
typedef struct
{
ElemType data;
/* 游标(Cursor),为0时表示无指向 */
int cur;
} Component,
/* 对于不提供结构struct的程序设计语言,
可以使用一对并行数组data和cur来处理。 */
StaticLinkList[MAXSIZE];
对数组第一个和最后一个元素作为特殊元素处理,不存数据。通常把未被使用的数组元素称为备用链表。
数组第一个元素,即下标为0的元素的cur存放备用链表的第一个结点的下标;
数组的最后一个元素的cur存放第一个有数值的元素的下标,相当于单链表中的头结点作用,当整个链表为空时,则为0。
此时的图示相当于初始化的数组状态,见下面代码:
/* 将一维数组space中各分量链成一备用链表, */
/* space[0].cur为头指针,"0"表示空指针 */
Status InitList(StaticLinkList space)
{
int i;
for (i = 0; i < MAXSIZE - 1; i++)
space[i].cur = i + 1;
/* 目前静态链表为空,最后一个元素的cur为0 */
space[MAXSIZE - 1].cur = 0;
return OK;
}
假设静态链表分别存放着“甲”、“乙”、“丁”、“戊”、“己”、“庚”等数据,则它将处于如下图所示这种状态。
“庚”是最后一个有值元素,所以它的cur设置为0。
最后一个元素的cur则因“甲”是第一有值元素而存有它的下标为1。
第一个元素则因空闲空间的第一个元素下标为7,所以它的cur是7。
为了辨明数组中哪些分量未被使用,解决的办法是将所有未被使用过的及已被删除的分量用游标链成一个备用的链表,每当进行插入时,便可以从备用链表上取得第一个结点作为待插入的新结点。
/* 若备用空间链表非空,则返回分配的结点下标 */
int Malloc_SLL(StaticLinkList space)
{
/* 当前数组第一个元素的cur值就是要返回的第一个备用空闲的下标 */
int i = space[0].cur;
/* 把它的下一个分量用来做备用 */
if (space[0].cur)
space[0].cur = space[i].cur;
return i;
}
从上面的图示例子来看,返回7。
总的来说,静态链表其实是为了给没有指针的高级语言设计的一种实现单链表能力的方法。
静态链表完整演示代码:
#include<stdio.h>
#include<stdlib.h>
#define MAXSIZE 1000
int visit(char c)
{
printf("%c ",c);
return 1;
}
/* 线性表的静态链表存储结构 */
typedef struct
{
char data;
int cur; /* 游标(Cursor) ,为0时表示无指向 */
} Component,StaticLinkList[MAXSIZE];
/* 将一维数组space中各分量链成一个备用链表,space[0].cur为头指针,"0"表示空指针 */
int InitList(StaticLinkList space)
{
int i;
for (i=0; i<MAXSIZE-1; i++)
space[i].cur = i+1;
space[MAXSIZE-1].cur = 0; /* 目前静态链表为空,最后一个元素的cur为0 */
return 1;
}
/* 若备用空间链表非空,则返回分配的结点下标,否则返回0 */
int Malloc_SSL(StaticLinkList space)
{
int i = space[0].cur; /* 当前数组第一个元素的cur存的值 */
/* 就是要返回的第一个备用空闲的下标 */
if (space[0]. cur)
space[0]. cur = space[i].cur; /* 由于要拿出一个分量来使用了, */
/* 所以我们就得把它的下一个 */
/* 分量用来做备用 */
return i;
}
/* 将下标为k的空闲结点回收到备用链表 */
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;
}
/* 在L中第i个元素之前插入新的数据元素e */
int ListInsert(StaticLinkList L, int i, char e)
{
int j, k, l;
k = MAXSIZE - 1; /* 注意k首先是最后一个元素的下标 */
if (i < 1 || i > ListLength(L) + 1)
return 0;
j = Malloc_SSL(L); /* 获得空闲分量的下标 */
if (j)
{
L[j].data = e; /* 将数据赋值给此分量的data */
for(l = 1; l <= i - 1; l++) /* 找到第i个元素之前的位置 */
k = L[k].cur;
L[j].cur = L[k].cur; /* 把第i个元素之前的cur赋值给新元素的cur */
L[k].cur = j; /* 把新元素的下标赋值给第i个元素之前元素的cur */
return 1;
}
return 0;
}
/* 删除在L中第i个数据元素 */
int ListDelete(StaticLinkList L, int i)
{
int j, k;
if (i < 1 || i > ListLength(L))
return 0;
k = MAXSIZE - 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 1;
}
int ListTraverse(StaticLinkList L)//遍历
{
int j=0;
int i=L[MAXSIZE-1].cur;
while(i)
{
visit(L[i].data);
i=L[i].cur;
j++;
}
return j;
printf("\n");
return 1;
}
int main()
{
StaticLinkList L;
int i;
i=InitList(L);
printf("初始化L后:L.length=%d\n",ListLength(L));
i=ListInsert(L,1,'F');
i=ListInsert(L,1,'E');
i=ListInsert(L,1,'D');
i=ListInsert(L,1,'B');
i=ListInsert(L,1,'A');
printf("\n在L的表头依次插入FEDBA后:\nL.data=");
ListTraverse(L);
i=ListInsert(L,3,'C');
printf("\n在L的“B”与“D”之间插入“C”后:\nL.data=");
ListTraverse(L);
i=ListDelete(L,1);
printf("\n在L的删除“A”后:\nL.data=");
ListTraverse(L);
printf("\n");
system("pause");
return 0;
}
循环链表
将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环
在单链表中,有了头结点时,我们可以用O(1)的时间访问第一个结点,但对于要访问到最后一个结点,却需要O(n)时间
改造这个循环链表,不用头指针,而是用指向终端结点的尾指针来表示循环链表,此时查找开始结点和终端结点都很方便了。
终端结点用尾指针rear指示,则查找终端结点是O(1),而开始结点,其实就是rear->next->next,其时间复杂也为O(1)。
要将两个循环链表合并成一个表时,有了尾指针就非常简单了。比如下面的这两个循环链表,它们的尾指针分别是rearA和rearB。
要想把它们合并,只需要如下的操作即可
/* 保存A表的头结点,即 ① */
p = rearA->next;
/*将本是指向B表的第一个结点(不是头结点)赋值给reaA->next,即 ②*/
rearA->next = rearB->next->next;
/* 将原A表的头结点赋值给rearB->next,即 ③ */
rearB->next = p;
双向链表
在双向链表中的结点都有两个指针域,一个指向直接后继,另一个指向直接前驱。
/* 双向链表存储结构 */
typedef struct DulNode
{
ElemType data;
struct DuLNode *prior; /* 直接前驱指针 */
struct DuLNode *next; /* 直接后继指针 */
} DulNode, *DuLinkList;
非空的循环的带头结点的双向链表如图所示。
插入操作时,顺序很重要,千万不能写反
/* 把p赋值给s的前驱,如图中 ① */
s->prior = p;
/* 把p->next赋值给s的后继,如图中 ② */
s->next = p->next;
/* 把s赋值给p->next的前驱,如图中 ③ */
p->next->prior = s;
/* 把s赋值给p的后继,如图中 ④ */
p->next = s;
删除结点p,只需要下面两步骤
p->prior->next = p->next;
p->next->prior = p->prior;
free(p);
栈与队列
栈的顺序存储结构及实现
允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom)
栈是一个线性表
插入和删除操作,我们改名为push和pop,英文直译是压和弹
下标为0的一端作为栈底比较好
当栈存在一个元素时,top等于0,因此通常把空栈的判定条件定为top等于-1
#include<stdio.h>
#include<stdlib.h>
#define MAXSIZE 20
/* 顺序栈结构 */
typedef struct
{
int data[MAXSIZE];
int top; /* 用于栈顶指针 */
}SqStack;
int visit(int c)
{
printf("%d ",c);
return 1;
}
/* 构造一个空栈S */
int InitStack(SqStack *S)
{
S->top=-1;
return 1;
}
/* 把S置为空栈 */
int ClearStack(SqStack *S)
{
S->top=-1;
return 1;
}
/* 若栈S为空栈,则返回1,否则返回0 */
int StackEmpty(SqStack S)
{
if (S.top==-1)
return 1;
else
return 0;
}
/* 返回S的元素个数,即栈的长度 */
int StackLength(SqStack S)
{
return S.top+1;
}
/* 若栈不空,则用e返回S的栈顶元素,并返回1;否则返回0 */
int GetTop(SqStack S,int *e)
{
if (S.top==-1)
return 0;
else
*e=S.data[S.top];
return 1;
}
/* 插入元素e为新的栈顶元素 */
int Push(SqStack *S,int e)
{
if(S->top == MAXSIZE -1) /* 栈满 */
{
return 0;
}
S->top++; /* 栈顶指针增加一 */
S->data[S->top]=e; /* 将新插入元素赋值给栈顶空间 */
return 1;
}
/* 若栈不空,则删除S的栈顶元素,用e返回其值,并返回1;否则返回0 */
int Pop(SqStack *S,int *e)
{
if(S->top==-1)
return 0;
*e=S->data[S->top]; /* 将要删除的栈顶元素赋值给e */
S->top--; /* 栈顶指针减一 */
return 1;
}
/* 从栈底到栈顶依次对栈中每个元素显示 */
int StackTraverse(SqStack S)
{
int i;
i=0;
while(i<=S.top)
{
visit(S.data[i++]);
}
printf("\n");
return 1;
}
int main()
{
int j;
SqStack s;
int e;
if(InitStack(&s)==1)
for(j=1;j<=10;j++)
Push(&s,j);
printf("栈中元素依次为:");
StackTraverse(s);
Pop(&s,&e);
printf("弹出的栈顶元素 e=%d\n",e);
printf("栈空否:%d(1:空 0:否)\n",StackEmpty(s));
GetTop(s,&e);
printf("栈顶元素 e=%d 栈的长度为%d\n",e,StackLength(s));
ClearStack(&s);
printf("清空栈后,栈空否:%d(1:空 0:否)\n",StackEmpty(s));
system("pause");
return 0;
}
入栈出栈均没有涉及到任何循环语句,时间复杂度均是O(1)
两栈共享空间
使用这样的数据结构,通常都是当两个栈的空间需求有相反关系时,也就是一个栈增长时另一个栈在缩短的情况。
让一个栈的栈底为数组的始端,即下标为0处,另一个栈为数组的末端,即下标为数组长度n-1处。这样,两个栈如果增加元素,就是两端点向中间延伸。
top1+1==top2为栈满
#include<stdio.h>
#include<stdlib.h>
#define MAXSIZE 20
/* 两栈共享空间结构 */
typedef struct
{
int data[MAXSIZE];
int top1; /* 栈1栈顶指针 */
int top2; /* 栈2栈顶指针 */
}SqDoubleStack;
int visit(int c)
{
printf("%d ",c);
return 1;
}
/* 构造一个空栈S */
int InitStack(SqDoubleStack *S)
{
S->top1=-1;
S->top2=MAXSIZE;
return 1;
}
/* 把S置为空栈 */
int ClearStack(SqDoubleStack *S)
{
S->top1=-1;
S->top2=MAXSIZE;
return 1;
}
/* 若栈S为空栈,则返回1,否则返回0 */
int StackEmpty(SqDoubleStack S)
{
if (S.top1==-1 && S.top2==MAXSIZE)
return 1;
else
return 0;
}
/* 返回S的元素个数,即栈的长度 */
int StackLength(SqDoubleStack S)
{
return (S.top1+1)+(MAXSIZE-S.top2);
}
/* 插入元素e为新的栈顶元素 */
int Push(SqDoubleStack *S,int e,int stackNumber)
{
if (S->top1+1==S->top2) /* 栈已满,不能再push新元素了 */
return 0;
if (stackNumber==1) /* 栈1有元素进栈 */
S->data[++S->top1]=e; /* 若是栈1则先top1+1后给数组元素赋值。 */
else if (stackNumber==2) /* 栈2有元素进栈 */
S->data[--S->top2]=e; /* 若是栈2则先top2-1后给数组元素赋值。 */
return 1;
}
/* 若栈不空,则删除S的栈顶元素,用e返回其值,并返回1;否则返回0 */
int Pop(SqDoubleStack *S,int *e,int stackNumber)
{
if (stackNumber==1)
{
if (S->top1==-1)
return 0; /* 说明栈1已经是空栈,溢出 */
*e=S->data[S->top1--]; /* 将栈1的栈顶元素出栈 */
}
else if (stackNumber==2)
{
if (S->top2==MAXSIZE)
return 0; /* 说明栈2已经是空栈,溢出 */
*e=S->data[S->top2++]; /* 将栈2的栈顶元素出栈 */
}
return 1;
}
int StackTraverse(SqDoubleStack S)
{
int i;
i=0;
while(i<=S.top1)
{
visit(S.data[i++]);
}
i=S.top2;
while(i<MAXSIZE)
{
visit(S.data[i++]);
}
printf("\n");
return 1;
}
int main()
{
int j;
SqDoubleStack s;
int e;
if(InitStack(&s)==1)
{
for(j=1;j<=5;j++)
Push(&s,j,1);
for(j=MAXSIZE;j>=MAXSIZE-2;j--)
Push(&s,j,2);
}
printf("栈中元素依次为:");
StackTraverse(s);
printf("当前栈中元素有:%d \n",StackLength(s));
Pop(&s,&e,2);
printf("弹出的栈顶元素 e=%d\n",e);
printf("栈空否:%d(1:空 0:否)\n",StackEmpty(s));
for(j=6;j<=MAXSIZE-2;j++)
Push(&s,j,1);
printf("栈中元素依次为:");
StackTraverse(s);
printf("栈满否:%d(1:否 0:满)\n",Push(&s,100,1));
ClearStack(&s);
printf("清空栈后,栈空否:%d(1:空 0:否)\n",StackEmpty(s));
system("pasue");
return 0;
}
栈的链式存储结构及实现
单链表有头指针,而栈顶指针也是必须的,所以比较好的办法是把栈顶放在单链表的头部。
有了栈顶在头部,单链表中比较常用的头结点也就失去了意义,通常对于链栈来说,是不需要头结点的
链栈的空是 top=NULL
#include<stdio.h>
#include<stdlib.h>
#define MAXSIZE 20
/* 链栈结构 */
typedef struct StackNode
{
int data;
struct StackNode *next;
}StackNode,*LinkStackPtr;
//注意*LinkStackPtr之前是逗号,类似于int a,b; 的逗号
//等价于 typedef struct StackNode* LinkStackPtr
typedef struct
{
LinkStackPtr top; //头指针
int count; //栈内元素数量
}LinkStack;
/* 构造一个空栈S */
int InitStack(LinkStack *S)
{
S->top = (LinkStackPtr)malloc(sizeof(StackNode));
if(!S->top)
return 0;
S->top=NULL;
S->count=0;
return 1;
}
/* 把S置为空栈 */
int ClearStack(LinkStack *S)
{
LinkStackPtr p,q;
p=S->top;
while(p)
{
q=p;
p=p->next;
free(q);
}
S->count=0;
return 1;
}
/* 若栈S为空栈,则返回1,否则返回0 */
int StackEmpty(LinkStack S)
{
if (S.count==0)
return 1;
else
return 0;
}
/* 返回S的元素个数,即栈的长度 */
int StackLength(LinkStack S)
{
return S.count;
}
/* 若栈不空,则用e返回S的栈顶元素,并返回1;否则返回0 */
int GetTop(LinkStack S,int *e)
{
if (S.top==NULL)
return 0;
else
*e=S.top->data;
return 1;
}
/* 插入元素e为新的栈顶元素 */
int Push(LinkStack *S,int e)
{
LinkStackPtr s=(LinkStackPtr)malloc(sizeof(StackNode));
s->data=e;
s->next=S->top; /* 把当前的栈顶元素赋值给新结点的直接后继*/
S->top=s; /* 将新的结点s赋值给栈顶指针 */
S->count++;
return 1;
}
/* 若栈不空,则删除S的栈顶元素,用e返回其值,并返回1;否则返回0 */
int Pop(LinkStack *S,int *e)
{
LinkStackPtr p;
if(StackEmpty(*S))
return 0;
*e=S->top->data;
p=S->top; /* 将栈顶结点赋值给p*/
S->top=S->top->next; /* 使得栈顶指针下移一位,指向后一结点*/
free(p);
S->count--;
return 1;
}
int StackTraverse(LinkStack S)
{
LinkStackPtr p;
p=S.top;
while(p)
{
printf("%d ",p->data);
p=p->next;
}
printf("\n");
return 1;
}
int main()
{
int j;
LinkStack s;
int e;
if(InitStack(&s)==1)
for(j=1;j<=10;j++)
Push(&s,j);
printf("栈中元素依次为:");
StackTraverse(s);
Pop(&s,&e);
printf("弹出的栈顶元素 e=%d\n",e);
printf("栈空否:%d(1:空 0:否)\n",StackEmpty(s));
GetTop(s,&e);
printf("栈顶元素 e=%d 栈的长度为%d\n",e,StackLength(s));
ClearStack(&s);
printf("清空栈后,栈空否:%d(1:空 0:否)\n",StackEmpty(s));
system("pause");
return 0;
}
栈的应用
递归
在前行阶段,对于每一层递归,函数的局部变量、参数值以及返回地址都被压入栈中。在退回阶段,位于栈顶的局部变量、参数值和返回地址被弹出,用于返回调用层次中执行代码的其余部分,也就是恢复了调用的状态。
四则运算表达式求值
括号都是成对出现的,有左括号就一定会有右括号,对于多重括号,最终也是完全嵌套匹配的。这用栈结构正好合适,只要碰到左括号,就将此左括号进栈,后面出现右括号时,就让栈顶的左括号出栈,期间让数字运算,这样,最终有括号的表达式从左到右巡查一遍
后缀表达式计算结果
对于“9+(3-1)×3+10÷2”,用后缀表示法应该是:“9 3 1-3*+102/+”,这样的表达式称为后缀表达式
规则:从左到右遍历表达式的每个数字和符号,遇到是数字就进栈,遇到是符号,就将处于栈顶两个数字出栈,进行运算,运算结果进栈,一直到最终获得结果。
下面,推导如何让“9+(3-1)×3+10÷2”转化为“9 3 1-3*+10 2/+”。
中缀表达式转后缀表达式
平时所用的“9+(3-1)×3+10÷2”叫做中缀表达式。
中缀表达式“9+(3-1)×3+10÷2”转化为后缀表达式“9 3 1-3*+10 2/+”。
规则:从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后缀表达式的一部分;若是符号,则判断其与栈顶符号的优先级,是右括号或优先级不高于栈顶符号(乘除优先加减)则栈顶元素依次出栈并输出,并将当前符号进栈,一直到最终输出后缀表达式为止。
1.初始化一空栈,用来对符号进出栈使用。
2.第一个字符是数字9,输出9,后面是符号“+”,进栈。如图4-9-6的右图所示。
3.第三个字符是“(”,依然是符号,因其只是左括号,还未配对,故进栈。如图4-9-7的左图所示。
4.第四个字符是数字3,输出,总表达式为93,接着是“-”,进栈。
5.接下来是数字1,输出,总表达式为 9 31,后面是符号“)”,此时,我们需要去匹配此前的“(”,所以栈顶依次出栈,并输出,直到“(”出栈为止。此时左括号上方只有“-”,因此输出“-”。总的输出表达式为 9 3 1-。
6.紧接着是符号“×”,因为此时的栈顶符号为“+”号,优先级低于“×”,因此不输出,“*”进栈。接着是数字3,输出,总的表达式为 9 3 1-3。
7.之后是符号“+”,此时当前栈顶元素“ * ”比这个“+”的优先级高,因此栈中元素出栈并输出(没有比“+”号更低的优先级,所以全部出栈),总输出表达式为9 3 1-3 * +。然后将当前这个符号“+”进栈。也就是说,前6张图的栈底的“+”是指中缀表达式中开头的9后面那个“+”,而图4-9-9左图中的栈底(也是栈顶)的“+”是指“9+(3-1)×3+”中的最后一个“+”。
8.紧接着数字10,输出,总表达式变为9 31-3 *+10。后是符号“÷”,所以“/”进栈。
9.最后一个数字2,输出,总的表达式为9 31-3+10 2。如图4-9-10的左图所示。10.因已经到最后,所以将栈中符号全部出栈并输出。最终输出的后缀表达式结果为93 1-3 *+10 2/+。
队列
队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的先进先出的线性表。
队列顺序存储的缺点
数组下标为0的一端即是队头。队列中的所有元素都得向前移动,以保证队列的队头,也就是下标为0的位置不为空,此时时间复杂度为O(n)
如果不去限制队列的元素必须存储在数组的前n个单元这一条件,出队的性能就会大大增加。也就是说,队头不需要一定在下标为0的位置
为了避免当只有一个元素时,队头和队尾重合使处理变得麻烦,所以引入两个指针,front指针指向队头元素,rear指针指向队尾元素的下一个位置,这样当front等于rear时,此队列不是还剩一个元素,而是空队列。
假设是长度为5的数组,初始状态,空队列如图所示,front与rear指针均指向下标为0的位置。然后入队a1、a2、a3、a4,front指针依然指向下标为0位置,而rear指针指向下标为4的位置
出队a1、a2,则front指针指向下标为2的位置,rear不变,如图所示,再入队a5,此时front指针不变,rear指针移动到数组之外。
这样会产生数组越界的错误,可实际上,我们的队列在下标为0和1的地方还是空闲的。我们把这种现象叫做“假溢出”。
循环队列
解决假溢出的办法就是循环队列。
问题来了,空队列时,front等于rear,现在当队列满时,也是front等于rear,那么如何判断此时的队列究竟是空还是满呢?
当队列空时,条件就是front=rear,当队列满时,我们修改其条件,保留一个元素空间。也就是说,队列满时,数组中还有一个空闲单元。
由于rear可能比front大,也可能比front小,所以尽管它们只相差一个位置时就是满的情况,但也可能是相差整整一圈。所以若队列的最大尺寸为QueueSize,那么队列满的条件是(rear+1)%QueueSize==front
比如上面这个例子,QueueSize=5,左图中front=0,而rear=4,(4+1)%5=0,所以此时队列满。右图,front=2而rear=1。(1+1)%5=2,所以此时队列也是满的。
而对于上图,front=2而rear=0,(0+1)%5=1,1≠2,所以此时队列并没有满
另外,当rear>front时,队列的长度为rear-front。
但当rear<front时,队列长度分为两段,一段是QueueSize-front,另一段是rear-0,加在一起,队列长度为rear-front+QueueSize。
因此通用的计算队列长度公式为:(rear-front+QueueSize)%QueueSize
#include<stdio.h>
#include<stdlib.h>
#define MAXSIZE 20
/* 循环队列的顺序存储结构 */
typedef struct
{
int data[MAXSIZE];
int front; /* 头指针 */
int rear; /* 尾指针,若队列不空,指向队列尾元素的下一个位置 */
}SqQueue;
int visit(int c)
{
printf("%d ",c);
return 1;
}
/* 初始化一个空队列Q */
int InitQueue(SqQueue *Q)
{
Q->front=0;
Q->rear=0;
return 1;
}
/* 将Q清为空队列 */
int ClearQueue(SqQueue *Q)
{
Q->front=Q->rear=0;
return 1;
}
/* 若队列Q为空队列,则返回1,否则返回0 */
int QueueEmpty(SqQueue Q)
{
if(Q.front==Q.rear) /* 队列空的标志 */
return 1;
else
return 0;
}
/* 返回Q的元素个数,也就是队列的当前长度 */
int QueueLength(SqQueue Q)
{
return (Q.rear-Q.front+MAXSIZE)%MAXSIZE;
}
/* 若队列不空,则用e返回Q的队头元素,并返回1,否则返回0 */
int GetHead(SqQueue Q,int *e)
{
if(Q.front==Q.rear) /* 队列空 */
return 0;
*e=Q.data[Q.front];
return 1;
}
/* 若队列未满,则插入元素e为Q新的队尾元素 */
int EnQueue(SqQueue *Q,int e)
{
if ((Q->rear+1)%MAXSIZE == Q->front) /* 队列满的判断 */
return 0;
Q->data[Q->rear]=e; /* 将元素e赋值给队尾 */
Q->rear=(Q->rear+1)%MAXSIZE;/* rear指针向后移一位置, */
/* 若到最后则转到数组头部 */
return 1;
}
/* 若队列不空,则删除Q中队头元素,用e返回其值 */
int DeQueue(SqQueue *Q,int *e)
{
if (Q->front == Q->rear) /* 队列空的判断 */
return 0;
*e=Q->data[Q->front]; /* 将队头元素赋值给e */
Q->front=(Q->front+1)%MAXSIZE; /* front指针向后移一位置, */
/* 若到最后则转到数组头部 */
return 1;
}
/* 从队头到队尾依次对队列Q中每个元素输出 */
int QueueTraverse(SqQueue Q)
{
int i;
i=Q.front;
while((i+Q.front)!=Q.rear)
{
visit(Q.data[i]);
i=(i+1)%MAXSIZE;
}
printf("\n");
return 1;
}
int main()
{
int j;
int i=0,l;
int d;
SqQueue Q;
InitQueue(&Q);
printf("初始化队列后,队列空否?%u(1:空 0:否)\n",QueueEmpty(Q));
printf("请输入整型队列元素(不超过%d个),-1为提前结束符: ",MAXSIZE-1);
do
{
/* scanf("%d",&d); */
d=i;
if(d==-1)
break;
i++;
EnQueue(&Q,d);
}while(i<MAXSIZE-1);
printf("队列长度为: %d\n",QueueLength(Q));
printf("现在队列空否?%u(1:空 0:否)\n",QueueEmpty(Q));
printf("连续%d次由队头删除元素,队尾插入元素:\n",MAXSIZE);
for(l=1;l<=MAXSIZE;l++)
{
DeQueue(&Q,&d);
printf("删除的元素是%d,插入的元素:%d \n",d,l+1000);
/* scanf("%d",&d); */
d=l+1000;
EnQueue(&Q,d);
}
l=QueueLength(Q);
printf("现在队列中的元素为: \n");
QueueTraverse(Q);
printf("共向队尾插入了%d个元素\n",i+MAXSIZE);
if(l-2>0)
printf("现在由队头删除%d个元素:\n",l-2);
while(QueueLength(Q)>2)
{
DeQueue(&Q,&d);
printf("删除的元素值为%d\n",d);
}
j=GetHead(Q,&d);
if(j)
printf("现在队头元素为: %d\n",d);
ClearQueue(&Q);
printf("清空队列后, 队列空否?%u(1:空 0:否)\n",QueueEmpty(Q));
system("pause");
return 0;
}
队列的链式存储结构
为了操作上的方便,我们将队头指针指向链队列的头结点,而队尾指针指向终端结点
空队列时,front和rear都指向头结点
入队操作时,其实就是在链表尾部插入结点
出队操作时,就是头结点的后继结点出队,将头结点的后继改为它后面的结点,若链表除头结点外只剩一个元素时,则需将rear指向头结点
#include<stdio.h>
#include<stdlib.h>
#define MAXSIZE 20
typedef struct QNode /* 结点结构 */
{
int data;
struct QNode* next;
}QNode,* QueuePtr;
typedef struct /* 队列的链表结构 */
{
QueuePtr front,rear; /* 队头、队尾指针 */
}LinkQueue;
/* 构造一个空队列Q */
int InitQueue(LinkQueue *Q)
{
Q->front=Q->rear=(QueuePtr)malloc(sizeof(QNode));
Q->front->next=NULL;
return 1;
}
/* 销毁队列Q */
int DestroyQueue(LinkQueue *Q)
{
while(Q->front)
{
Q->rear=Q->front->next;
free(Q->front);
Q->front=Q->rear;
}
return 1;
}
/* 将Q清为空队列 */
int ClearQueue(LinkQueue *Q)
{
QueuePtr p,q;
Q->rear=Q->front;
p=Q->front->next;
Q->front->next=NULL;
while(p)
{
q=p;
p=p->next;
free(q);
}
return 1;
}
/* 若Q为空队列,则返回1,否则返回0 */
int QueueEmpty(LinkQueue Q)
{
if(Q.front==Q.rear)
return 1;
else
return 0;
}
/* 求队列的长度 */
int QueueLength(LinkQueue Q)
{
int i=0;
QueuePtr p;
p=Q.front;
while(Q.rear!=p)
{
i++;
p=p->next;
}
return i;
}
/* 若队列不空,则用e返回Q的队头元素,并返回1,否则返回0 */
int GetHead(LinkQueue Q,int *e)
{
QueuePtr p;
if(Q.front==Q.rear)
return 0;
p=Q.front->next;
*e=p->data;
return 1;
}
/* 插入元素e为Q的新的队尾元素 */
int EnQueue(LinkQueue *Q,int e)
{
QueuePtr s=(QueuePtr)malloc(sizeof(QNode));//新建一个结点,因为函数结束后结点需要保留,因此分配内存
s->data=e;
s->next=NULL;
Q->rear->next=s; /* 把拥有元素e的新结点s赋值给原队尾结点的后继 */
Q->rear=s; /* 把当前的s设置为队尾结点,rear指向s */
return 1;
}
/* 若队列不空,删除Q的队头元素,用e返回其值,并返回1,否则返回0 */
int DeQueue(LinkQueue *Q,int *e)
{
QueuePtr p;
if(Q->front==Q->rear)
return 0;
p=Q->front->next; /* 将欲删除的队头结点暂存给p */
*e=p->data; /* 将欲删除的队头结点的值赋值给e */
Q->front->next=p->next;/* 将原队头结点的后继p->next赋值给头结点后继 */
if(Q->rear==p) /* 若队头就是队尾,则删除后将rear指向头结点 */
Q->rear=Q->front;
free(p);
return 1;
}
/* 从队头到队尾依次对队列Q中每个元素输出 */
int QueueTraverse(LinkQueue Q)
{
QueuePtr p;
p=Q.front->next;
while(p)
{
printf("%d ",p->data);
p=p->next;
}
printf("\n");
return 1;
}
int main()
{
int i;
int d;
LinkQueue q;
i=InitQueue(&q);
if(i)
printf("成功地构造了一个空队列!\n");
printf("是否空队列?%d(1:空 0:否) ",QueueEmpty(q));
printf("队列的长度为%d\n",QueueLength(q));
EnQueue(&q,-5);
EnQueue(&q,5);
EnQueue(&q,10);
printf("队列的元素依次为:");
QueueTraverse(q);
i=GetHead(q,&d);
if(i==1)
printf("队头元素是:%d\n",d);
DeQueue(&q,&d);
printf("删除了队头元素%d\n",d);
i=GetHead(q,&d);
if(i==1)
printf("新的队头元素是:%d\n",d);
ClearQueue(&q);
printf("清空队列后,q.front=%p q.rear=%p q.front->next=%p\n",q.front,q.rear,q.front->next);
DestroyQueue(&q);
printf("销毁队列后,q.front=%p q.rear=%p\n",q.front, q.rear);
system("pause");
return 0;
}
在可以确定队列长度最大值的情况下,建议用循环队列,如果你无法预估队列的长度时,则用链队列。
串
串的链式存储结构除了在连接串与串操作时有一定方便之外,总的来说不如顺序存储灵活,性能也不如顺序存储结构好。
朴素模式的匹配算法
最好的情况是一开始就区配成功,时间复杂度为O(1)。差一些,那么时间复杂度为O(n+m),其中n为主串长度,m为要匹配的子串长度。根据等概率原则,平均是(n+m)/2次查找,时间复杂度为O(n+m)。
最坏的情况是每次不成功的匹配都发生在串T的最后一个字符。举一个很极端的例子。主串为S=“00000000000000000000000000000000000000000000000001”,而要匹配的子串为T=“0000000001”,前者是有49个“0”和1个“1”的主串,后者是9个“0”和1个“1”的子串。在匹配时,每次都得将T中字符循环到最后一位才发现不匹配。这样等于T串需要在S串的前40个位置都需要判断10次,并得出不匹配的结论。直到最后第41个位置,因为全部匹配相等,所以不需要再继续进行下去。如果最终没有可匹配的子串,比如是T=“0000000002”,到了第41位置判断不匹配后同样不需要继续比对下去。因此最坏情况的时间复杂度为O((n-m+1)*m)。
串操作代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define MAXSIZE 40
typedef char String[MAXSIZE+1]; /* 0号单元存放串的长度 */
/* 生成一个值等于chars的串T */
int StrAssign(String T,char *chars)
{
int i;
if(strlen(chars)>MAXSIZE)
return 0;
else
{
T[0]=strlen(chars);
for(i=1;i<=T[0];i++)
T[i]=*(chars+i-1);
return 1;
}
}
/* 复制串S得串T */
int StrCopy(String T,String S)
{
int i;
for(i=0;i<=S[0];i++)
T[i]=S[i];
return 1;
}
/* 判空 */
int StrEmpty(String S)
{
if(S[0]==0)
return 1;
else
return 0;
}
/* 操作结果: 若S>T,则返回值>0;若S=T,则返回值=0;若S<T,则返回值<0 */
int StrCompare(String S,String T)
{
int i;
for(i=1;i<=S[0]&&i<=T[0];++i)
if(S[i]!=T[i])
return S[i]-T[i];
return S[0]-T[0];
}
/* 返回串长 */
int StrLength(String S)
{
return S[0];
}
/* 将S清为空串 */
int ClearString(String S)
{
S[0]=0;
return 1;
}
/* 用T返回S1和S2联接而成的新串。若未截断,则返回1,否则0 */
int Concat(String T,String S1,String S2)
{
int i;
if(S1[0]+S2[0]<=MAXSIZE) /* 未截断 */
{
for(i=1;i<=S1[0];i++)
T[i]=S1[i];
for(i=1;i<=S2[0];i++)
T[S1[0]+i]=S2[i];
T[0]=S1[0]+S2[0];
return 1;
}
else /* 截断S2 */
{
for(i=1;i<=S1[0];i++)
T[i]=S1[i];
for(i=1;i<=MAXSIZE-S1[0];i++)
T[S1[0]+i]=S2[i];
T[0]=MAXSIZE;
return 0;
}
}
/* 用Sub返回串S的第pos个字符起长度为len的子串。 */
int SubString(String Sub,String S,int pos,int len)
{
int i;
if(pos<1||pos>S[0]||len<0||len>S[0]-pos+1)
return 0;
for(i=1;i<=len;i++)
Sub[i]=S[pos+i-1];
Sub[0]=len;
return 1;
}
/* 返回子串T在主串S中第pos个字符之后的位置。若不存在,则函数返回值为0。 */
int Index(String S, String T, int pos)
{
int i = pos; /* i用于主串S中当前位置下标值*/
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;
}
/* 若主串S中第pos个字符之后存在与T相等的子串, */
/* 则返回第一个这样的子串在S中的位置,否则返回0 */
int Index2(String S, String T, int pos)
{
int n,m,i;
String sub;
if (pos > 0)
{
n = StrLength(S);
m = StrLength(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;
}
/* 在串S的第pos个字符之前插入串T。完全插入返回1,部分插入返回0 */
int StrInsert(String S,int pos,String T)
{
int i;
if(pos<1||pos>S[0]+1)
return 0;
if(S[0]+T[0]<=MAXSIZE) /* 完全插入 */
{
for(i=S[0];i>=pos;i--)
S[i+T[0]]=S[i]; //后移
for(i=pos;i<pos+T[0];i++)
S[i]=T[i-pos+1];
S[0]=S[0]+T[0];
return 1;
}
else /* 部分插入 */
{
for(i=MAXSIZE;i>=pos;i--)
S[i]=S[i-T[0]];
for(i=pos;i<pos+T[0];i++)
S[i]=T[i-pos+1];
S[0]=MAXSIZE;
return 0;
}
}
/* 从串S中删除第pos个字符起长度为len的子串 */
int StrDelete(String S,int pos,int len)
{
int i;
if(pos<1||pos>S[0]-len+1||len<0)
return 0;
for(i=pos+len;i<=S[0];i++)
S[i-len]=S[i];
S[0]-=len;
return 1;
}
/* 用V替换主串S中出现的所有与T相等的不重叠的子串 */
int Replace(String S,String T,String V)
{
int i=1; /* 从串S的第一个字符起查找串T */
if(StrEmpty(T)) /* T是空串 */
return 0;
do
{
i=Index(S,T,i); /* 结果i为从上一个i之后找到的子串T的位置 */
if(i) /* 串S中存在串T */
{
StrDelete(S,i,StrLength(T)); /* 删除该串T */
StrInsert(S,i,V); /* 在原串T的位置插入串V */
i+=StrLength(V); /* 在插入的串V后面继续查找串T */
}
}while(i);
return 1;
}
/* 输出字符串T */
void StrPrint(String T)
{
int i;
for(i=1;i<=T[0];i++)
printf("%c",T[i]);
printf("\n");
}
int main()
{
int i,j,k;
char s;
String t,s1,s2;
printf("请输入串s1: ");
k=StrAssign(s1,"abcd");
if(!k)
{
printf("串长超过MAXSIZE(=%d)\n",MAXSIZE);
exit(0);
}
printf("串长为%d\n",StrLength(s1));
StrCopy(s2,s1);
printf("拷贝s1生成的串为: ");
StrPrint(s2);
printf("请输入串s2: ");
k=StrAssign(s2,"efghijk");
if(!k)
{
printf("串长超过MAXSIZE(%d)\n",MAXSIZE);
exit(0);
}
i=StrCompare(s1,s2);
if(i<0)
s='<';
else if(i==0)
s='=';
else
s='>';
printf("串s1%c串s2\n",s);
k=Concat(t,s1,s2);
printf("串s1联接串s2得到的串t为: ");
StrPrint(t);
if(k==0)
printf("串t有截断\n");
ClearString(s1);
printf("清为空串后,串s1为: ");
StrPrint(s1);
printf("求串t的子串,请输入子串的起始位置,子串长度: ");
i=2;j=3;
printf("%d,%d \n",i,j);
k=SubString(s2,t,i,j);
if(k)
{
printf("子串s2为: ");
StrPrint(s2);
}
printf("从串t的第pos个字符起,删除len个字符,请输入pos,len: ");
i=4;j=2;
printf("%d,%d \n",i,j);
StrDelete(t,i,j);
printf("删除后的串t为: ");
StrPrint(t);
i=StrLength(s2)/2;
StrInsert(s2,i,t);
printf("在串s2的第%d个字符之前插入串t后,串s2为:\n",i);
StrPrint(s2);
i=Index(s2,t,1);
printf("s2的第%d个字母起和t第一次匹配\n",i);
SubString(t,s2,1,1);
printf("串t为:");
StrPrint(t);
Concat(s1,t,t);
printf("串s1为:");
StrPrint(s1);
Replace(s2,t,s1);
printf("用串s1取代串s2中和串t相同的不重叠的串后,串s2为: ");
StrPrint(s2);
system("pause");
return 0;
}
KMP模式匹配算法
KMP算法可以大大避免重复遍历的情况。
这里也需要强调,KMP算法仅当模式与主串之间存在许多“部分匹配”的情况下才体现出它的优势,否则优势并不明显。
KMP模式匹配算法原理
算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。
把T串各个位置的j值的变化定义为一个数组next,那么next的长度就是T串的长度。于是我们可以得到下面的函数定义:
next数组值推导
手算:
模式串中第j个位置与主串中第i个位置发生不匹配时,应从模式串中第next[j]个位置与主串第i个位置重新开始比较
next[1]为0,为特殊标记,表示应从模式串第一个字符与主串当前不匹配字符的下一个字符开始比较
next[j]的值等于公共前后缀长度+1
[ 当前不相同的位与模式串的第(其之前相同位的最长公共前后缀+1)位进行比较 ]
代码思路:
KMP模式匹配算法实现
定长存储结构
typedef struct
{
char str[maxSize+1];
int length;
}Str;
变长存储结构
typedef struct
{
char* ch;
int length;
}Str;
Str S;
S.length=L;
S.ch =(char*)malloc((L+1)*sizeof(char));
free(S.ch);
KMP:
/* 通过计算返回子串T的next数组(已知next[t-1]求next[t]) */
void get_next(String T, int *next)
{
int i, j;
i = 1;
j = 0;
next[1] = 0;
/* 此处T[0]表示串T的长度 */
while (i < T[0])
{
/* T[i]表示后缀的单个字符 */
/* T[j]表示前缀的单个字符 */
if (j == 0 || T[i] == T[j])
{
//next[i+1] = j+1;
i++;
j++;
next[i] = j;
}
else
/* 若字符不相同,则j值回溯 */
j = next[j];
}
}
/* 返回子串T在主串S中第pos个字符之后的位置。
若不存在,则函数返回值为0。 */
int Index_KMP(String S, String T, int pos)
{
int i = pos;
/* j用于子串T中当前位置下标值 */
int j = 1;
int next[255];
get_next(T, next);
while (i <= S[0] && j <= T[0])
{
/* 两字母相等则继续,相对于朴素算法增加了j=0判断(第一位不匹配时next[j]=0)*/
if (j == 0 || S[i] == T[j])
{
i++;
j++;
}
else
{
/* j退回合适的位置,i值不变 */
j = next[j];
}
}
if (j > T[0])
return i - T[0];
else
return 0;
}
KMP模式匹配算法改进
KMP还是有缺陷的。
比如,如果我们的主串S=“aaaabcde”,子串T=“aaaaax”
S: a a a a b c d e
T: a a a a a x
next: 0 1 2 3 4 5
nextval: 0 0 0 0 4 5
id: 1 2 3 4 5 6
如下图,开始时,i=5、j=5,“b”与“a”不相等,因此j=next[5]=4,此时“b”与第4位置的“a”依然不等,j=next[4]=3,直到j=next[1]=0时,根据算法,此时i++、j++,得到i=6、j=1。
我们发现,当中的②③④⑤步骤,其实是多余的判断。那么可以用首位next[1]的值去取代与它相等的字符后续next[j]的值。
于是对求next函数进行改良,next修正为nextval
nextval数组值推导
总结改进过的KMP算法,它是在计算出next值的同时:
如果a位字符与它next值指向的b位字符相等,则该a位的nextval就指向b位的nextval值
如果不等,则该a位的nextval值就是它自己a位的next的值。
代码如下:
/* 求模式串T的next函数修正值并存入数组nextval */
void get_nextval(String T, int *nextval)
{
int i, j;
i = 1;
j = 0;
nextval[1] = 0;
while (i < T[0])
{
/* T[i]表示后缀的单个字符, */
/* T[j]表示前缀的单个字符 */
if (j == 0 || T[i] == T[j])
{
i++;
j++;
/* 若当前字符与前缀字符不同 */
if (T[i] != T[j])
/* 则当前的j为nextval在i位置的值 */
nextval[i] = j;
else
/*相同则将前缀字符的nextval值赋值给nextval在i位置的值*/
nextval[i] = nextval[j];
}
else
j = nextval[j]; /* 若字符不相同,则j值回溯 */
}
}
串完整操作代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define MAXSIZE 100 /* 存储空间初始分配量 */
typedef char String[MAXSIZE+1]; /* 0号单元存放串的长度 */
/* 生成一个其值等于chars的串T */
int StrAssign(String T,char *chars)
{
int i;
if(strlen(chars)>MAXSIZE)
return 0;
else
{
T[0]=strlen(chars);
for(i=1;i<=T[0];i++)
T[i]=*(chars+i-1);
return 1;
}
}
int ClearString(String S)
{
S[0]=0;/* 令串长为零 */
return 1;
}
/* 输出字符串T。 */
void StrPrint(String T)
{
int i;
for(i=1;i<=T[0];i++)
printf("%c",T[i]);
printf("\n");
}
/* 输出Next数组值。 */
void NextPrint(int next[],int length)
{
int i;
for(i=1;i<=length;i++)
printf("%d",next[i]);
printf("\n");
}
/* 返回串的元素个数 */
int StrLength(String S)
{
return S[0];
}
/* 朴素的模式匹配法 */
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;
}
/* 通过计算返回子串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的长度 */
{
if(j==0 || T[i]== T[j]) /* T[i]表示后缀的单个字符,T[j]表示前缀的单个字符 */
{
++i;
++j;
next[i] = j;
}
else
j= next[j]; /* 若字符不相同,则j值回溯 */
}
}
/* 返回子串T在主串S中第pos个字符之后的位置。若不存在,则函数返回值为0。 */
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的长度时,循环继续 */
{
if (j==0 || S[i] == T[j]) /* 两字母相等则继续,与朴素算法增加了j=0判断 */
{
++i;
++j;
}
else /* 指针后退重新开始匹配 */
j = next[j];/* j退回合适的位置,i值不变 */
}
if (j > T[0])
return i-T[0];
else
return 0;
}
/* 求模式串T的next函数修正值并存入数组nextval */
void get_nextval(String T, int *nextval)
{
int i,j;
i=1;
j=0;
nextval[1]=0;
while (i<T[0]) /* 此处T[0]表示串T的长度 */
{
if(j==0 || T[i]== T[j]) /* T[i]表示后缀的单个字符,T[j]表示前缀的单个字符 */
{
++i;
++j;
if (T[i]!=T[j]) /* 若当前字符与前缀字符不同 */
nextval[i] = j; /* 则当前的j为nextval在i位置的值 */
else
nextval[i] = nextval[j]; /* 如果与前缀字符相同,则将前缀字符的 */
/* nextval值赋值给nextval在i位置的值 */
}
else
j= nextval[j]; /* 若字符不相同,则j值回溯 */
}
}
int Index_KMP1(String S, String T, int pos)
{
int i = pos; /* i用于主串S中当前位置下标值,若pos不为1,则从pos位置开始匹配 */
int j = 1; /* j用于子串T中当前位置下标值 */
int next[255]; /* 定义一next数组 */
get_nextval(T, next); /* 对串T作分析,得到next数组 */
while (i <= S[0] && j <= T[0]) /* 若i小于S的长度并且j小于T的长度时,循环继续 */
{
if (j==0 || S[i] == T[j]) /* 两字母相等则继续,与朴素算法增加了j=0判断 */
{
++i;
++j;
}
else /* 指针后退重新开始匹配 */
j = next[j];/* j退回合适的位置,i值不变 */
}
if (j > T[0])
return i-T[0];
else
return 0;
}
int main()
{
int i,*p;
String s1,s2;
StrAssign(s1,"abcdex");
printf("子串为: ");
StrPrint(s1);
i=StrLength(s1);
p=(int*)malloc((i+1)*sizeof(int));
get_next(s1,p);
printf("Next为: ");
NextPrint(p,StrLength(s1));
printf("\n");
StrAssign(s1,"abcabx");
printf("子串为: ");
StrPrint(s1);
i=StrLength(s1);
p=(int*)malloc((i+1)*sizeof(int));
get_next(s1,p);
printf("Next为: ");
NextPrint(p,StrLength(s1));
printf("\n");
StrAssign(s1,"ababaaaba");
printf("子串为: ");
StrPrint(s1);
i=StrLength(s1);
p=(int*)malloc((i+1)*sizeof(int));
get_next(s1,p);
printf("Next为: ");
NextPrint(p,StrLength(s1));
printf("\n");
StrAssign(s1,"aaaaaaaab");
printf("子串为: ");
StrPrint(s1);
i=StrLength(s1);
p=(int*)malloc((i+1)*sizeof(int));
get_next(s1,p);
printf("Next为: ");
NextPrint(p,StrLength(s1));
printf("\n");
StrAssign(s1,"ababaaaba");
printf(" 子串为: ");
StrPrint(s1);
i=StrLength(s1);
p=(int*)malloc((i+1)*sizeof(int));
get_next(s1,p);
printf(" Next为: ");
NextPrint(p,StrLength(s1));
get_nextval(s1,p);
printf("NextVal为: ");
NextPrint(p,StrLength(s1));
printf("\n");
StrAssign(s1,"aaaaaaaab");
printf(" 子串为: ");
StrPrint(s1);
i=StrLength(s1);
p=(int*)malloc((i+1)*sizeof(int));
get_next(s1,p);
printf(" Next为: ");
NextPrint(p,StrLength(s1));
get_nextval(s1,p);
printf("NextVal为: ");
NextPrint(p,StrLength(s1));
printf("\n");
StrAssign(s1,"00000000000000000000000000000000000000000000000001");
printf("主串为: ");
StrPrint(s1);
StrAssign(s2,"0000000001");
printf("子串为: ");
StrPrint(s2);
printf("\n");
printf("主串和子串在第%d个字符处首次匹配(朴素模式匹配算法)\n",Index(s1,s2,1));
printf("主串和子串在第%d个字符处首次匹配(KMP算法) \n",Index_KMP(s1,s2,1));
printf("主串和子串在第%d个字符处首次匹配(KMP改良算法) \n",Index_KMP1(s1,s2,1));
system("pause");
return 0;
}
树
树的定义
对于树的定义需要强调两点:
- n>0时根结点是唯一的。
- m>0时,子树的个数没有限制,但它们一定是互不相交的。像下图中的两个结构就不符合树的定义,因为它们都有相交的子树。
树的结点包含一个数据元素及若干指向其子树的分支。结点拥有的子树数称为结点的度(De-gree)。
度为0的结点称为叶结点(Leaf)或终端结点;
度不为0的结点称为非终端结点或分支结点。除根结点之外,分支结点也称为内部结点。
树的度是树内各结点的度的最大值。如下图树的度为3。
结点间关系:
结点的祖先是从根到该结点所经分支上的所有结点。
对于H来说,D、B、A都是它的祖先。
反之,以某结点为根的子树中的任一结点都称为该结点的子孙。B的子孙有D、G、H、I
结点的层次(Level)从根开始定义起,根为第一层,根的孩子为第二层。若某结点在第l层,则其子树就在第l+1层。其双亲在同一层的结点互为堂兄弟。
如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树为有序树,否则称为无序树。
树的存储结构
三种不同的表示法:双亲表示法、孩子表示法、孩子兄弟表示法。
双亲表示法
除了根结点外,其余每个结点,它不一定有孩子,但是一定有且仅有一个双亲。
由于根结点是没有双亲的,所以我们约定根结点的位置域设置为-1
双亲表示法的结点结构定义代码:
#define MAX_TREE_SIZE 100
/* 结点结构 */
typedef struct PTNode
{
int data; // 结点数据
int parent; // 双亲位置
} PTNode;
/* 树结构 */
typedef struct
{
PTNode nodes[MAX_TREE_SIZE]; // 结点数组
int r, n; //根的位置和结点数
} PTree;
孩子表示法
由于树中每个结点可能有多棵子树,可以考虑用多重链表,即每个结点有多个指针域,其中每个指针指向一棵子树的根结点,这种方法叫做多重链表表示法。不过,树的每个结点的度,也就是它的孩子个数是不同的。所以可以设计两种方案来解决。
-
方案一:每个结点指针域的个数就等于树的度
这种方法对于树中各结点的度相差很大时,显然是很浪费空间的,因为有很多的结点,它的指针域都是空的。 -
方案二:每个结点指针域的个数等于该结点的度
取一个位置来存储结点指针域的个数
degree为度域
这种方法克服了浪费空间的缺点,但是由于各个结点的链表是不相同的结构,加上要维护结点的度的数值,在运算上就会带来时间上的损耗。
能否有更好的方法,既可以减少空指针的浪费又能使结点结构相同。
仔细观察,我们为了要遍历整棵树,把每个结点放到一个顺序存储结构的数组中是合理的,但每个结点的孩子有多少是不确定的,所以我们再对每个结点的孩子建立一个单链表体现它们的关系。
这就是我们要讲的孩子表示法。
具体办法是,把每个结点的孩子结点排列起来,以单链表作存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空。然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中,如下图所示。
为此,设计两种结点结构:
一个是孩子链表的孩子结点,其中child是数据域,用来存储某个结点在表头数组中的下标。next是指针域,用来存储指向某结点的下一个孩子结点的指针。
另一个是表头数组的表头结点。其中data是数据域,存储某结点的数据信息。firstchild是头指针域,存储该结点的孩子链表的头指针。
#define MAX_TREE_SIZE 100
/* 孩子结点 */
typedef struct CTNode
{
int child;
struct CTNode *next;
} * ChildPtr;
/* 表头结构 */
typedef struct
{
int data;
ChildPtr firstchild;
} CTBox;
/* 树结构 */
typedef struct
{
CTBox nodes[MAX_TREE_SIZE];
int r,n; //根的位置和结点数
} CTree;
这样的结构对于查找某个结点的某个孩子,或者某个结点的兄弟,只需要查找这个结点的孩子单链表即可。对于遍历整棵树也是很方便的,对头结点的数组循环即可。
但是,这也存在着问题,知道某个结点的双亲比较麻烦,需要整棵树遍历。所以把双亲表示法和孩子表示法综合一下,称为双亲孩子表示法,如下图所示:
#define MAX_TREE_SIZE 100
//孩子结点
typedef struct CTNode
{
int child;
struct CTNode *next;
} * ChildPtr;
//表头结构
typedef struct
{
int data;
int parent; //双亲下标
ChildPtr firstchild;
}CTBox;
//树结构
typedef struct
{
CTBox nodes[MAX_TREE_SIZE];
int r,n; // 根的位置和树中结点的总数
}PCTree;
孩子兄弟表示法
任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。因此,我们设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟。
data是数据域,firstchild为指针域,存储该结点的第一个孩子结点的存储地址,right-sib是指针域,存储该结点的右兄弟结点的存储地址。
/* 树的孩子兄弟表示法结构定义 */
typedef struct CSNode
{
int data;
struct CSNode *firstchild;
struct CSNode *rightsib;
} CSNode,*CSTree;
这种表示法,给查找某个结点的某个孩子带来了方便,只需要通过fistchild找到此结点的长子,然后再通过长子结点的rightsib找到它的二弟,接着一直下去,直到找到具体的孩子。当然,如果想找某个结点的双亲,这个表示法也是有缺陷的,如果真的有必要,完全可以再增加一个parent指针域来解决快速查找双亲的问题
其实这个表示法的最大好处是它把一棵复杂的树变成了一棵二叉树
二叉树的定义
二叉树特点
二叉树的特点有:
- 每个结点最多有两棵子树,注意不是只有两棵子树,而是最多有。
- 左子树和右子树是有顺序的,次序不能任意颠倒。
- 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。
二叉树具有五种基本形态:
1.空二叉树。
2.只有一个根结点。
3.根结点只有左子树。
4.根结点只有右子树。
5.根结点既有左子树又有右子树。
有三个结点的树对于二叉树来说,演变成五种不同的二叉树:
特殊二叉树
-
斜树
所有的结点都只有左子树的二叉树叫左斜树。所有结点都是只有右子树的二叉树叫右斜树。这两者统称为斜树。
斜树有很明显的特点,就是每一层都只有一个结点,结点的个数与二叉树的深度相同。
线性表可以理解为是树的一种特殊表现形式。 -
满二叉树
在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。
-
完全二叉树
对一棵具有n个结点的二叉树按层序编号,如果编号为i(1≤i≤n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树
完全二叉树的所有结点与同样深度的满二叉树,它们按层序编号相同的结点,是一一对应的。
像下图中的树1,因为5结点没有左子树,却有右子树,那就使得按层序编号的第10个编号空档了。
同样道理,图中的树2,由于3结点没有子树,所以使得6、7编号的位置空档了。树3又是因为5编号下没有子树造成第10和第11位置空档。上图中的树,尽管它不是满二叉树,但是编号是连续的,所以它是完全二叉树。
完全二叉树的特点:
1.叶子结点只能出现在最下两层。
2.最下层的叶子一定集中在左部连续位置。
3.倒数二层,若有叶子结点,一定都在右部连续位置。
4.如果结点度为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+1|(|x|表示不大于x的最大整数)
性质5:如果对一棵有n个结点的完全二叉树(其深度为k)的结点按层序编号(从第1层到第层,每层从左到右),对任一结点i(1≤i≤n)有:
1.如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点。
2.如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i。
3.如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1。
二叉树的存储结构
二叉树顺序存储结构
对于一般的二叉树,尽管层序编号不能反映逻辑关系,但是可以将其按完全二叉树编号,把不存在的结点设置为“∧”
考虑一种极端的情况:
一棵深度为k的右斜树,它只有k个结点,需要分配2k-1个存储单元空间,这显然是对存储空间的浪费,如下图所示。所以,顺序存储结构一般只用于完全二叉树。
/*二叉树顺序结构实现*/
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
#include<time.h>
#define MAX_TREE_SIZE 100 /* 二叉树的最大结点数 */
int Nil=0;
typedef int SqBiTree[MAX_TREE_SIZE]; /* 0号单元存储根结点 */
typedef struct
{
int level,order; /* 结点的层,本层序号(按满二叉树计算) */
}Position;
int visit(int c)
{
printf("%d ",c);
return 1;
}
/* 构造空二叉树T。因为T是固定数组,不会改变,故不需要& */
int InitBiTree(SqBiTree T)
{
int i;
for(i=0;i<MAX_TREE_SIZE;i++)
T[i]=Nil; /* 初值为空 */
return 1;
}
/* 按层序次序输入二叉树中结点的值, 构造顺序存储的二叉树T */
int CreateBiTree(SqBiTree T)
{
int i=0;
printf("请按层序输入结点的值(整型),结点数≤%d:\n",MAX_TREE_SIZE);
while(i<10)
{
T[i]=i+1;
if(i!=0&&T[(i+1)/2-1]==Nil&&T[i]!=Nil) /* 此结点(不空)无双亲且不是根 */
{
printf("出现无双亲的非根结点%d\n",T[i]);
}
i++;
}
while(i<MAX_TREE_SIZE)
{
T[i]=Nil; /* 将空赋值给T的后面的结点 */
i++;
}
return 1;
}
#define ClearBiTree InitBiTree /* 在顺序存储结构中,两函数完全一样 */
/* 若T为空二叉树,则返回1,否则0 */
int BiTreeEmpty(SqBiTree T)
{
if(T[0]==Nil) /* 根结点为空,则树空 */
return 1;
else
return 0;
}
/* 返回T的深度 */
int BiTreeDepth(SqBiTree T)
{
int i,j=-1;
for(i=MAX_TREE_SIZE-1;i>=0;i--) /* 找到最后一个结点 */
if(T[i]!=Nil)
break;
i++;
do
j++;
while(i>=powl(2,j));/* 计算2的j次幂。 */
return j;
}
/* 当T不空,用e返回T的根,返回1;否则返回0,e无定义 */
int Root(SqBiTree T,int *e)
{
if(BiTreeEmpty(T)) /* T空 */
return 0;
else
{
*e=T[0];
return 1;
}
}
/* 返回处于位置e(层,本层序号)的结点的值 */
int Value(SqBiTree T,Position e)
{
return T[(int)powl(2,e.level-1)+e.order-2];
}
/* 给处于位置e(层,本层序号)的结点赋新值value */
int Assign(SqBiTree T,Position e,int value)
{
int i=(int)powl(2,e.level-1)+e.order-2; /* 将层、本层序号转为矩阵的序号 */
if(value!=Nil&&T[(i+1)/2-1]==Nil) /* 给叶子赋非空值但双亲为空 */
return 0;
else if(value==Nil&&(T[i*2+1]!=Nil||T[i*2+2]!=Nil)) /* 给双亲赋空值但有叶子(不空) */
return 0;
T[i]=value;
return 1;
}
/* 若e是T的非根结点,则返回它的双亲,否则返回"空" */
int Parent(SqBiTree T,int e)
{
int i;
if(T[0]==Nil) /* 空树 */
return Nil;
for(i=1;i<=MAX_TREE_SIZE-1;i++)
if(T[i]==e) /* 找到e */
return T[(i+1)/2-1];
return Nil; /* 没找到e */
}
/* 返回e的左孩子。若e无左孩子,则返回"空" */
int LeftChild(SqBiTree T,int e)
{
int i;
if(T[0]==Nil) /* 空树 */
return Nil;
for(i=0;i<=MAX_TREE_SIZE-1;i++)
if(T[i]==e) /* 找到e */
return T[i*2+1];
return Nil; /* 没找到e */
}
/* 返回e的右孩子。若e无右孩子,则返回"空" */
int RightChild(SqBiTree T,int e)
{
int i;
if(T[0]==Nil) /* 空树 */
return Nil;
for(i=0;i<=MAX_TREE_SIZE-1;i++)
if(T[i]==e) /* 找到e */
return T[i*2+2];
return Nil; /* 没找到e */
}
/* 返回e的左兄弟。若e是T的左孩子或无左兄弟,则返回"空" */
int LeftSibling(SqBiTree T,int e)
{
int i;
if(T[0]==Nil) /* 空树 */
return Nil;
for(i=1;i<=MAX_TREE_SIZE-1;i++)
if(T[i]==e&&i%2==0) /* 找到e且其序号为偶数(是右孩子) */
return T[i-1];
return Nil; /* 没找到e */
}
/* 返回e的右兄弟。若e是T的右孩子或无右兄弟,则返回"空" */
int RightSibling(SqBiTree T,int e)
{
int i;
if(T[0]==Nil) /* 空树 */
return Nil;
for(i=1;i<=MAX_TREE_SIZE-1;i++)
if(T[i]==e&&i%2) /* 找到e且其序号为奇数(是左孩子) */
return T[i+1];
return Nil; /* 没找到e */
}
/* PreOrderTraverse()调用 */
void PreTraverse(SqBiTree T,int e)
{
visit(T[e]);
if(T[2*e+1]!=Nil) /* 左子树不空 */
PreTraverse(T,2*e+1);
if(T[2*e+2]!=Nil) /* 右子树不空 */
PreTraverse(T,2*e+2);
}
/* 先序遍历T */
int PreOrderTraverse(SqBiTree T)
{
if(!BiTreeEmpty(T)) /* 树不空 */
PreTraverse(T,0);
printf("\n");
return 1;
}
/* InOrderTraverse()调用 */
void InTraverse(SqBiTree T,int e)
{
if(T[2*e+1]!=Nil) /* 左子树不空 */
InTraverse(T,2*e+1);
visit(T[e]);
if(T[2*e+2]!=Nil) /* 右子树不空 */
InTraverse(T,2*e+2);
}
/* 中序遍历T。 */
int InOrderTraverse(SqBiTree T)
{
if(!BiTreeEmpty(T)) /* 树不空 */
InTraverse(T,0);
printf("\n");
return 1;
}
/* PostOrderTraverse()调用 */
void PostTraverse(SqBiTree T,int e)
{
if(T[2*e+1]!=Nil) /* 左子树不空 */
PostTraverse(T,2*e+1);
if(T[2*e+2]!=Nil) /* 右子树不空 */
PostTraverse(T,2*e+2);
visit(T[e]);
}
/* 后序遍历T。 */
int PostOrderTraverse(SqBiTree T)
{
if(!BiTreeEmpty(T)) /* 树不空 */
PostTraverse(T,0);
printf("\n");
return 1;
}
/* 层序遍历二叉树 */
void LevelOrderTraverse(SqBiTree T)
{
int i=MAX_TREE_SIZE-1,j;
while(T[i]==Nil)
i--; /* 找到最后一个非空结点的序号 */
for(j=0;j<=i;j++) /* 从根结点起,按层序遍历二叉树 */
if(T[j]!=Nil)
visit(T[j]); /* 只遍历非空的结点 */
printf("\n");
}
/* 逐层、按本层序号输出二叉树 */
void Print(SqBiTree T)
{
int j,k;
Position p;
int e;
for(j=1;j<=BiTreeDepth(T);j++)
{
printf("第%d层: ",j);
for(k=1;k<=powl(2,j-1);k++)
{
p.level=j;
p.order=k;
e=Value(T,p);
if(e!=Nil)
printf("%d:%d ",k,e);
}
printf("\n");
}
}
int main()
{
int i,e;
Position p;
SqBiTree T;
InitBiTree(T);
CreateBiTree(T);
printf("建立二叉树后,树空否?%d(1:是 0:否) 树的深度=%d\n",BiTreeEmpty(T),BiTreeDepth(T));
i=Root(T,&e);
if(i)
printf("二叉树的根为:%d\n",e);
else
printf("树空,无根\n");
printf("层序遍历二叉树:\n");
LevelOrderTraverse(T);
printf("前序遍历二叉树:\n");
PreOrderTraverse(T);
printf("中序遍历二叉树:\n");
InOrderTraverse(T);
printf("后序遍历二叉树:\n");
PostOrderTraverse(T);
printf("修改结点的层号3本层序号2。");
p.level=3;
p.order=2;
e=Value(T,p);
printf("待修改结点的原值为%d请输入新值:50 ",e);
e=50;
Assign(T,p,e);
printf("前序遍历二叉树:\n");
PreOrderTraverse(T);
printf("结点%d的双亲为%d,左右孩子分别为",e,Parent(T,e));
printf("%d,%d,左右兄弟分别为",LeftChild(T,e),RightChild(T,e));
printf("%d,%d\n",LeftSibling(T,e),RightSibling(T,e));
ClearBiTree(T);
printf("清除二叉树后,树空否?%d(1:是 0:否) 树的深度=%d\n",BiTreeEmpty(T),BiTreeDepth(T));
i=Root(T,&e);
if(i)
printf("二叉树的根为:%d\n",e);
else
printf("树空,无根\n");
system("pause");
return 0;
}
二叉链表
二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域,称这样的链表叫做二叉链表。
/* 二叉树的二叉链表结点结构定义 */
/* 结点结构 */
typedef struct BiTNode
{
TElemType data;
struct BiTNode *lchild, *rchild; // 左右孩子指针
} BiTNode, *BiTree;
如果有需要,还可以再增加一个指向其双亲的指针域,那样就称之为三叉链表。
/*二叉树链式结构实现*/
#include<string.h>
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
#include<time.h>
#define MAXSIZE 100
/* 用于构造二叉树* */
int index=1;
typedef char String[24]; /* 0号单元存放串的长度 */
String str;
int StrAssign(String T,char *chars)
{
int i;
if(strlen(chars)>MAXSIZE)
return 0;
else
{
T[0]=strlen(chars);
for(i=1;i<=T[0];i++)
T[i]=*(chars+i-1);
return 1;
}
}
typedef char TElemType;
TElemType Nil=' '; /* 字符型以空格符为空 */
int visit(TElemType e)
{
printf("%c ",e);
return 1;
}
typedef struct BiTNode /* 结点结构 */
{
TElemType data; /* 结点数据 */
struct BiTNode *lchild,*rchild; /* 左右孩子指针 */
}BiTNode,*BiTree;
/* 构造空二叉树T */
int InitBiTree(BiTree *T)
{
*T=NULL;
return 1;
}
/* 销毁二叉树T */
void DestroyBiTree(BiTree *T)
{
if(*T)
{
if((*T)->lchild) /* 有左孩子 */
DestroyBiTree(&(*T)->lchild); /* 销毁左孩子子树 */
if((*T)->rchild) /* 有右孩子 */
DestroyBiTree(&(*T)->rchild); /* 销毁右孩子子树 */
free(*T); /* 释放根结点 */
*T=NULL; /* 空指针赋0 */
}
}
/* 按前序输入二叉树中结点的值(一个字符) */
/* #表示空树,构造二叉链表表示二叉树T。 */
void CreateBiTree(BiTree *T)
{
TElemType ch;
/* scanf("%c",&ch); */
ch=str[index++];
if(ch=='#')
*T=NULL;
else
{
*T=(BiTree)malloc(sizeof(BiTNode));
(*T)->data=ch; /* 生成根结点 */
CreateBiTree(&(*T)->lchild); /* 构造左子树 */
CreateBiTree(&(*T)->rchild); /* 构造右子树 */
}
}
/* 若T为空二叉树,则返回1,否则0 */
int BiTreeEmpty(BiTree T)
{
if(T)
return 0;
else
return 1;
}
#define ClearBiTree DestroyBiTree
/* 返回T的深度 */
int BiTreeDepth(BiTree T)
{
int i,j;
if(!T)
return 0;
if(T->lchild)
i=BiTreeDepth(T->lchild);
else
i=0;
if(T->rchild)
j=BiTreeDepth(T->rchild);
else
j=0;
return i>j?i+1:j+1;
}
/* 返回T的根 */
TElemType Root(BiTree T)
{
if(BiTreeEmpty(T))
return Nil;
else
return T->data;
}
/* 返回p所指结点的值 */
TElemType Value(BiTree p)
{
return p->data;
}
/* 给p所指结点赋值为value */
void Assign(BiTree p,TElemType value)
{
p->data=value;
}
/* 前序递归遍历T */
void PreOrderTraverse(BiTree T)
{
if(T==NULL)
return;
printf("%c",T->data);/* 显示结点数据,可以更改为其它对结点操作 */
PreOrderTraverse(T->lchild); /* 再先序遍历左子树 */
PreOrderTraverse(T->rchild); /* 最后先序遍历右子树 */
}
/* 中序递归遍历T */
void InOrderTraverse(BiTree T)
{
if(T==NULL)
return;
InOrderTraverse(T->lchild); /* 中序遍历左子树 */
printf("%c",T->data);/* 显示结点数据,可以更改为其它对结点操作 */
InOrderTraverse(T->rchild); /* 最后中序遍历右子树 */
}
/* 后序递归遍历T */
void PostOrderTraverse(BiTree T)
{
if(T==NULL)
return;
PostOrderTraverse(T->lchild); /* 先后序遍历左子树 */
PostOrderTraverse(T->rchild); /* 再后序遍历右子树 */
printf("%c",T->data);/* 显示结点数据,可以更改为其它对结点操作 */
}
int main()
{
int i;
BiTree T;
TElemType e1;
InitBiTree(&T);
StrAssign(str,"ABDH#K###E##CFI###G#J##");
CreateBiTree(&T);
printf("构造空二叉树后,树空否?%d(1:是 0:否) 树的深度=%d\n",BiTreeEmpty(T),BiTreeDepth(T));
e1=Root(T);
printf("二叉树的根为: %c\n",e1);
printf("\n前序遍历二叉树:");
PreOrderTraverse(T);
printf("\n中序遍历二叉树:");
InOrderTraverse(T);
printf("\n后序遍历二叉树:");
PostOrderTraverse(T);
ClearBiTree(&T);
printf("\n清除二叉树后,树空否?%d(1:是 0:否) 树的深度=%d\n",BiTreeEmpty(T),BiTreeDepth(T));
i=Root(T);
if(!i)
printf("树空,无根\n");
system("pause");
return 0;
}
遍历二叉树
二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。
二叉树遍历方法
二叉树的遍历方式可以很多,如果我们限制了从左到右的习惯方式,那么主要就分为四种:
-
前序遍历
规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。如下图所示,遍历的顺序为:ABDGH-CEIF。
-
中序遍历
规则是若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。如下图所示,遍历的顺序为:GDHBAE-ICF。
-
后序遍历
规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。如下图所示,遍历的顺序为:GHDBIEFCA。
-
层序遍历
规则是若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。如下图所示,遍历的顺序为:ABCDEFGHI。
四种遍历方法,其实都是在把树中的结点变成某种意义的线性序列,这就给程序的实现带来了好处。另外不同的遍历提供了对结点依次处理的不同方式,可以在遍历过程中对结点进行各种处理。
前序遍历算法
二叉树的定义是用递归的方式,所以,实现遍历算法也可以采用递归,而且极其简洁明了。
/* 二叉树的前序遍历递归算法 */
void PreOrderTraverse(BiTree T)
{
if (T == NULL)
return;
/* 显示结点数据,可以更改为其他对结点操作 */
printf("%c", T->data);
/* 再先序遍历左子树 */
PreOrderTraverse(T->lchild);
/* 最后先序遍历右子树 */
PreOrderTraverse(T->rchild);
}
中序遍历算法
换句话说,它等于是把调用左孩子的递归函数提前了,就这么简单。
/* 二叉树的中序遍历递归算法 */
void InOrderTraverse(BiTree T)
{
if (T == NULL)
return;
/* 中序遍历左子树 */
InOrderTraverse(T->lchild);
/* 显示结点数据,可以更改为其他对结点操作 */
printf("%c", T->data);
/* 最后中序遍历右子树 */
InOrderTraverse(T->rchild);
}
后序遍历算法
/* 二叉树的后序遍历递归算法 */
void PostOrderTraverse(BiTree T)
{
if (T == NULL)
return;
/* 先后序遍历左子树 */
PostOrderTraverse(T->lchild);
/* 再后序遍历右子树 */
PostOrderTraverse(T->rchild);
/* 显示结点数据,可以更改为其他对结点操作 */
printf("%c", T->data);
}
推导遍历结果
已知一棵二叉树的前序遍历序列为ABCDEF,中序遍历序列为CBAEDF,请问这棵二叉树的后序遍历结果是多少?
三种遍历都是从根结点开始,前序遍历是先打印再递归左和右。所以前序遍历序列为ABCDEF,第一个字母是A被打印出来,就说明A是根结点的数据。再由中序遍历序列是CBAEDF,可以知道C和B是A的左子树的结点,E、D、F是A的右子树的结点,如下图所示。
然后看前序中的C和B,它的顺序是ABCDEF,是先打印B后打印C,所以B应该是A的左孩子,而C就只能是B的孩子,此时是左还是右孩子还不确定。再看中序序列是CBAEDF,C是在B的前面打印,这就说明C是B的左孩子,否则就是右孩子了,如下图所示。
再看前序中的E、D、F,它的顺序是ABCDEF,那就意味着D是A结点的右孩子,E和F是D的子孙,注意,它们中有一个不一定是孩子,还有可能是孙子的。再来看中序序列是CBAEDF,由于E在D的左侧,而F在右侧,所以可以确定E是D的左孩子,F是D的右孩子。因此最终得到的二叉树是下图所示。
为了避免推导中的失误,最好在心中递归遍历,检查一下这棵树的前序和中序遍历序列是否与题目中的相同。
复原了二叉树,后序遍历结果就是CBEFDA。
反过来,如果我们的题目是这样:二叉树的中序序列是ABCDEFG,后序序列是BDCAFGE,求前序序列。
由后序的BDCAFGE,得到E是根结点,因此前序首字母是E。
于是根据中序序列分为两棵树ABCD和FG,由后序序列的BDCAFGE,知道A是E的左孩子,前序序列目前分析为EA。
再由中序序列的ABCDEFG,知道BCD是A结点的右子孙,再由后序序列的BDCAFGE知道C结点是A结点的右孩子,前序序列目前分析得到EAC。
中序序列ABCDEFG,得到B是C的左孩子,D是C的右孩子,所以前序序列目前分析结果为EACBD。
由后序序列BDCAFGE,得到G是E的右孩子,于是F就是G的孩子。根本不需关心F是G的左还是右孩子,前序遍历序列的最终结果就是EACBDGF。
不过分析可以得出F是G的左孩子。
从这里我们也得到两个二叉树遍历的性质。
- 已知前序遍历序列和中序遍历序列,可以唯一确定一棵二叉树。
- 已知后序遍历序列和中序遍历序列,可以唯一确定一棵二叉树。
二叉树的建立
如果要在内存中建立一个如图下左图的树,为了能让每个结点确认是否有左右孩子,对它进行扩展,变成下右图的样子,也就是将二叉树中每个结点的空指针引出一个虚结点,其值为一特定值,比如“#”。
称这种处理后的二叉树为原二叉树的扩展二叉树。扩展二叉树就可以做到一个遍历序列确定一棵二叉树了。如上图的前序遍历序列就为AB#D##C##。
typedef struct BiTNode /* 结点结构 */
{
char data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
/* 按前序输入二叉树中结点的值(一个字符) */
/* #表示空树,构造二叉链表表示二叉树T */
void CreateBiTree(BiTree *T)
{
char ch;
scanf("%c", &ch);
if (ch == '#')
*T = NULL;
else
{
*T = (BiTree)malloc(sizeof(BiTNode));
/* 生成根结点 */
(*T)->data = ch;
/* 构造左子树 */
CreateBiTree(&(*T)->lchild);
//函数改变的的是BiTNode类型的指针,所以传入BiTNode类型指针的指针
//所以用到取址符
//(*T)->lchild是一个BiTNode类型的指针,
//需要取这个指针的地址作为二级指针传入函数
/* 构造右子树 */
CreateBiTree(&(*T)->rchild);
}
}
其实建立二叉树,也是利用了递归的原理。只不过在原来应该是打印结点的地方,改成了生成结点、给结点赋值的操作而已。
线索二叉树
线索二叉树原理
上图指针域并不是都充分的利用了,有许许多多的“∧”,也就是空指针域的存在
在做遍历时,得到了HDIBEJAFCG这样的字符序列,遍历过后可以知道任意一个结点的前驱和后继。
可是这是建立在已经遍历过的基础之上的。在二叉链表上,只能知道每个结点指向其左右孩子结点的地址,而不知道某个结点的前驱和后继。每次需要知道时,都必须先遍历一次。
综合两个角度分析后,考虑利用那些空地址,存放指向结点在某种遍历次序下的前驱和后继结点的地址。把这种指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树(Threaded Binary Tree)。
如上图,把这棵二叉树进行中序遍历后,将所有的空指针域中的rchild,改为指向它的后继结点。于是我们就可以通过指针知道H的后继是D,I的后继是B,G的后继因为不存在而指向NULL。
将这棵二叉树的所有空指针域中的lchild,改为指向当前结点的前驱。因此H的前驱是NULL,I的前驱是D。
容易看出,线索二叉树等于是把一棵二叉树转变成了一个双向链表,这样对插入删除结点、查找结点都带来了方便。
对二叉树以某种次序遍历使其变为线索二叉树的过程称做是线索化。
不过问题没有彻底解决。如何知道某一结点的lchild是指向它的左孩子还是指向前驱?rchild是指向右孩子还是指向后继?
需要一个区分标志。因此在每个结点再增设两个标志域ltag和rtag,注意ltag和rtag只是存放0或1数字的布尔型变量,其占用的内存空间要小于像lchild和rchild的指针变量。
- ltag为0时指向该结点的左孩子,为1时指向该结点的前驱。
- rtag为0时指向该结点的右孩子,为1时指向该结点的后继。
线索二叉树结构实现
线索化的实质就是将二叉链表中的空指针改为指向前驱或后继的线索。
由于前驱和后继的信息只有在遍历该二叉树时才能得到,所以线索化的过程就是在遍历的过程中修改空指针的过程。
/* 二叉树的二叉线索存储结构定义 */
/* Link==0表示指向左右孩子指针 */
/* Thread==1表示指向前驱或后继的线索 */
typedef enum {Link, Thread} PointerTag;
/* 二叉线索存储结点结构 */
typedef struct BiThrNode
{
int 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;
/* 前驱右孩子指针指向后继(当前结点p) */
pre->rchild = p;
}
/* 保持pre指向p的前驱 */
pre = p;
/* 递归右子树线索化 */
InThreading(p->rchild);
}
}
前驱较容易,后继麻烦一些。因为此时p结点的后继还没有访问到,只能完成它的前驱结点pre的线索化。
遍历线索二叉树其实就等于是操作一个双向链表结构。
和双向链表结构一样,在二叉树线索链表上添加一个头结点,如下图所示,令其lchild域的指针指向二叉树的根结点,rchild域的指针指向中序遍历时访问的最后一个结点。反之,令二叉树的中序序列中的第一个结点中,lchild域指针和最后一个结点的rchild域指针均指向头结点。这样定义的好处就是既可以从第一个结点起顺后继进行遍历,也可以从最后一个结点起顺前驱进行遍历。
/* 中序遍历二叉线索链表表示的二叉树T (非递归)*/
Status InOrderTraverse_Thr(BiThrTree T)
{
BiThrTree p; // p指向根结点
p = T->lchild;
/* T是头结点,空树或遍历结束时,p==T */
while (p != T)
{
/* 当LTag==0时循环到中序序列第一个结点 */
while (p->LTag == Link)
p = p->lchild;
/* 显示结点数据,可以更改为其他对结点操作 */
printf("%c", p->data);
while (p->RTag == Thread && p->rchild != T)
{
p = p->rchild;
printf("%c", p->data);
}
/* p进至其右子树根 */
p = p->rchild;
}
}
它充分利用了空指针域的空间,又保证了创建时的一次遍历就可以永远获得前驱后继的信息。所以在实际问题中,如果所用的二叉树需经常遍历或查找结点时需要某种遍历序列中的前驱和后继,采用线索二叉链表的存储结构是不错的选择。
/*线索二叉树*/
#include<string.h>
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
#include<time.h>
#define MAXSIZE 100
typedef enum {Link,Thread} PointerTag;
/* Link==0表示指向左右孩子指针, */
/* Thread==1表示指向前驱或后继的线索 */
typedef struct BiThrNode /* 二叉线索存储结点结构 */
{
char data; /* 结点数据 */
struct BiThrNode *lchild, *rchild; /* 左右孩子指针 */
PointerTag LTag;
PointerTag RTag; /* 左右标志 */
} BiThrNode, *BiThrTree;
char Nil='#'; /* 字符型以#为空 */
int visit(char e)
{
printf("%c ",e);
return 1;
}
/* 按前序输入二叉线索树中结点的值,构造二叉线索树T */
/* 0(整型)/空格(字符型)表示空结点 */
int CreateBiThrTree(BiThrTree *T)
{
char h;
scanf("%c",&h);
if(h==Nil)
*T=NULL;
else
{
*T=(BiThrTree)malloc(sizeof(BiThrNode));
(*T)->data=h; /* 生成根结点(前序) */
CreateBiThrTree(&(*T)->lchild); /* 递归构造左子树 */
if((*T)->lchild) /* 有左孩子 */
(*T)->LTag=Link;
CreateBiThrTree(&(*T)->rchild); /* 递归构造右子树 */
if((*T)->rchild) /* 有右孩子 */
(*T)->RTag=Link;
}
return 1;
}
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); /* 递归右子树线索化 */
}
}
/* 中序遍历二叉树T,并将其中序线索化,Thrt指向头结点 */
int InOrderThreading(BiThrTree *Thrt,BiThrTree T)
{
*Thrt=(BiThrTree)malloc(sizeof(BiThrNode));
(*Thrt)->LTag=Link; /* 建头结点 */
(*Thrt)->RTag=Thread;
(*Thrt)->rchild=(*Thrt); /* 右指针回指 */
if(!T) /* 若二叉树空,则左指针回指 */
(*Thrt)->lchild=*Thrt;
else
{
(*Thrt)->lchild=T;
pre=(*Thrt); //pre初始值赋为头结点
InThreading(T); /* 中序遍历进行中序线索化 */
pre->rchild=*Thrt;
pre->RTag=Thread; /* 最后一个结点线索化 */
(*Thrt)->rchild=pre;
}
return 1;
}
/* 中序遍历二叉线索树T(头结点)的非递归算法 */
int InOrderTraverse_Thr(BiThrTree T)
{
BiThrTree p;
p=T->lchild; /* p指向根结点 */
while(p!=T)
{ /* 空树或遍历结束时,p==T */
while(p->LTag==Link)
p=p->lchild;
if(!visit(p->data))
return 0;
// 110 111两行相当于 visit(p->data)
//上面这样写是为了确保无错误
while(p->RTag==Thread&&p->rchild!=T)
{
p=p->rchild;
visit(p->data); /* 访问后继结点 */
}
p=p->rchild;
}
return 1;
}
int main()
{
BiThrTree H,T;
printf("请按前序输入二叉树(如:'ABDH##I##EJ###CF##G##')\n");
CreateBiThrTree(&T); /* 按前序产生二叉树 */
InOrderThreading(&H,T); /* 中序遍历,并中序线索化二叉树 */
printf("中序遍历(输出)二叉线索树:\n");
InOrderTraverse_Thr(H); /* 中序遍历(输出)二叉线索树 */
printf("\n");
system("pause");
return 0;
}
树、森林与二叉树的转换
树的孩子兄弟法可以将一棵树用二叉链表进行存储,所以借助二叉链表,树和二叉树可以相互进行转换。
树转换为二叉树
将树转换为二叉树的步骤如下
- 加线。在所有兄弟结点之间加一条连线。
- 去线。对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线。
- 层次调整。以树的根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明。注意第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子。
森林转换为二叉树
森林是由若干棵树组成的,所以完全可以理解为,森林中的每一棵树都是兄弟,可以按照兄弟的处理办法来操作
步骤:
- 把每个树转换为二叉树。
- 第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来。当所有的二叉树连接起来后就得到了由森林转换来的二叉树。
二叉树转换为树
二叉树转换为树是树转换为二叉树的逆过程,也就是反过来做而已。
步骤:
- 加线。若某结点的左孩子结点存在,则将左孩子的n个右孩子结点都作为此结点的孩子。将该结点与这些右孩子结点用线连接起来。
- 去线。删除原二叉树中所有结点与其右孩子结点的连线。
- 层次调整。使之结构层次分明。
二叉树转换为森林
判断一棵二叉树能否转换成一棵树还是森林,只要看这棵二叉树的根结点有没有右孩子,有就是森林,没有就是一棵树
步骤:
- 从根结点开始,若右孩子存在,则把与右孩子结点的连线删除,再查看分离后的二叉树,若右孩子存在,则连线删除,直到所有右孩子连线都删除为止,得到分离的二叉树。
- 再将每棵分离后的二叉树转换为树即可。
树与森林的遍历
树的遍历分为两种方式。一种是先根遍历树,即先访问树的根结点,然后依次先根遍历根的每棵子树。另一种是后根遍历,即先依次后根遍历每棵子树,然后再访问根结点。比如下图的树,它的先根遍历序列为ABEFCDG,后根遍历序列为EFBCGDA。
森林的遍历也分为两种方式:
1.前序遍历:先访问森林中第一棵树的根结点,然后再依次先根遍历根的每棵子树,再依次用同样方式遍历除去第一棵树的剩余树构成的森林。比如下图的森林,前序遍历序列的结果就是ABCDEFGHJI。
2.后序遍历:是先访问森林中第一棵树,后根遍历的方式遍历每棵子树,然后再访问根结点,再依次同样方式遍历除去第一棵树的剩余树构成的森林。比如下图的森林,后序遍历序列的结果就是BCDAFEJHIG。
分析发现,森林的前序遍历和二叉树的前序遍历结果相同,森林的后序遍历和二叉树的中序遍历结果相同。所以当以二叉链表作树的存储结构时,树的先根遍历和后根遍历完全可以借用二叉树的前序遍历和中序遍历的算法来实现。
赫夫曼树及其应用
平时所用的压缩和解压缩技术都是基于赫夫曼的研究之上发展而来
假如比例分布如下:
70分以上大约占总数80%的成绩都需要经过3次以上的判断才可以得到结果,这显然不合理。中等成绩(70~79分之间)比例最高,其次是良好成绩,不及格的所占比例最少。把二叉树重新进行分配。改成如下图的做法
应该效率要高一些了,到底高多少呢?见下:
赫夫曼树定义与原理
先把这两棵二叉树简化成叶子结点带权的二叉树(注:树结点间的边相关的数叫做权Weight),如下图所示。其中A表示不及格、B表示及格、C表示中等、D表示良好、E表示优秀。每个叶子的分支线上的数字就是五级分制的成绩所占百分比。
从树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数目称做路径长度。树的路径长度就是从树根到每一结点的路径长度之和。
二叉树a的树路径长度就为1+1+2+2+3+3+4+4=20。二叉树b的树路径长度就为1+2+3+3+2+1+2+2=16。
如果考虑到带权的结点,结点的带权的路径长度为从该结点到树根之间的路径长度与结点上权的乘积。树的带权路径长度为树中所有叶子结点的带权路径长度之和。假设有n个权值{w1,w2,…,wn},构造一棵有n个叶子结点的二叉树,每个叶子结点带权wk,每个叶子的路径长度为lk,则其中带权路径长度WPL最小的二叉树称做赫夫曼树。
二叉树a的WPL=5×1+15×2+40×3+30×4+10×4=315
二叉树b的WPL=5×3+15×3+40×2+30×2+10×2=220
这样的结果意味着如果有10000个学生的百分制成绩需要计算五级分制成绩,用二叉树a的判断方法,需要做31500次比较,而二叉树b的判断方法,只需要22000次比较
构造出赫夫曼树方法如下:
- 先把有权值的叶子结点按照从小到大的顺序排列成一个有序序列,即:A5,E10,B15,D30,C40。
- 取头两个最小权值的结点作为一个新节点N1的两个子结点,注意相对较小的是左孩子,这里就是A为N1的左孩子,E为N1的右孩子,如下图所示。新结点的权值为两个叶子权值的和5+10=15。
- 将N1替换A与E,插入有序序列中,保持从小到大排列。即:N115,B15,D30,C40。
- 重复步骤2。将N1与B作为一个新节点N2的两个子结点。如下图所示。N2的权值=15+15=30。
- 将N2替换N1与B,插入有序序列中,保持从小到大排列。即:N230,D30,C40。
- 重复步骤2。将N2与D作为一个新节点N3的两个子结点。如下图所示。N3的权值=30+30=60。
- 将N3替换N2与D,插入有序序列中,保持从小到大排列。即:C40,N360。
- 重复步骤2。将C与N3作为一个新节点T的两个子结点,如下图所示。由于T即是根结点,完成赫夫曼树的构造。
此时的二叉树的带权路径长度WPL=40×1+30×2+15×3+10×4+5×4=205。是最优的赫夫曼树。
赫夫曼编码
信息传输时一般用二进制
但信息传输时字母或汉字的出现频率是不相同的
假设六个字母的频率为A 27,B 8,C 15,D15,E 30,F 5,合起来正好是100%。那就意味着可以重新按照赫夫曼树来规划它们。
下左图为构造赫夫曼树的过程的权值显示。右图为将权值左分支改为0,右分支改为1后的赫夫曼树。
此时,对这六个字母用其从树根到叶子所经过路径的0或1来编码,可以得到如下所示的定义。
将文字内容再次编码,可以看到结果变小了。也就是数据被压缩了
压缩过的新编码解码:
编码中非0即1,长短不等容易混淆。要设计长短不等的编码,则必须是任一字符的编码都不是另一个字符的编码的前缀,这种编码称做前缀编码。
可这样不足以方便地解码,因此在解码时,还是要用到赫夫曼树,即发送方和接收方必须要约定好同样的赫夫曼编码规则。
当接收到1001010010101001000111100时,由约定好的赫夫曼树可知,1001得到第一个字母是B,接下来01意味着第二个字符是A,其余的也相应的可以得到,从而成功解码。
一般地,设需要编码的字符集为{d1,d2,…,dn},各个字符在电文中出现的次数或频率集合为{w1,w2,…,wn},以d1,d2,…,dn作为叶子结点,以w1,w2,…,wn作为相应叶子结点的权值来构造一棵赫夫曼树。规定赫夫曼树的左分支代表0,右分支代表1,则从根结点到叶子结点所经过的路径分支组成的0和1的序列便为该结点对应字符的编码,这就是赫夫曼编码。
图
图的定义
G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。
注意:
- 线性表中数据元素叫元素,树中数据元素叫结点,图中数据元素称为顶点(Vertex)。
- 线性表中可以没有数据元素,称为空表。树中可以没有结点,叫做空树。在图结构中,不允许没有顶点。
- 线性表中,相邻的数据元素之间具有线性关系,树结构中,相邻两层的结点具有层次关系,而图中,任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以是空的。
无向边用小括号“()”表示,有向边则用尖括号“<>”表示。
在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。含有n个顶点的无向完全图有n(n-1)/2条边
在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。含有n个顶点的有向完全图有n×(n-1)条边
带权(Weight)的图通常称为网(Network)
下图带底纹的图均为左侧无向图与有向图的子图。
对于无向图,顶点v的度(Degree)是和v相关联的边的数目,记为TD(v)
边数=各顶点度数和的一半
对于有向图,以顶点v为头的弧的数目称为v的入度(InDegree),记为ID(v);以v为尾的弧的数目称为v的出度(OutDegree),记为OD(v);顶点v的度为TD(v)=ID(v)+OD(v)。
树中根结点到任意结点的路径是唯一的,但是图中顶点与顶点之间的路径却是不唯一的。
第一个顶点和最后一个顶点相同的路径称为回路或环(Cycle)
序列中顶点不重复出现的路径称为简单路径。除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,称为简单回路或简单环。
上图两个图的粗线都构成环,左侧的环第一个顶点和最后一个顶点都是B,且C、D、A没有重复出现,是一个简单环。
右侧的环,顶点C重复,不是简单环。
极大连通子图:
1.连通图只有一个极大连通子图,就是它本身。(是唯一的)
2.非连通图有多个极大连通子图。(非连通图的极大连通子图叫做连通分量,每个分量都是一个连通图)
3.称为极大是因为如果此时加入任何一个不在图的点集中的点都会导致它不再连通。
无向
极小连通子图:
1.一个连通图的生成树是该连通图顶点集确定的极小连通子图。(同一个连通图可以有不同的生成树,所以生成树不是唯一的)
(极小连通子图只存在于连通图中)
2.用边把极小连通子图中所有节点给连起来,若有n个节点,则有n-1条边。如下图生成树有6个节点,有5条边。
3.之所以称为极小是因为此时如果删除一条边,就无法构成生成树,也就是说给极小连通子图的每个边都是不可少的。
4.如果在生成树上添加一条边,一定会构成一个环。
也就是说只要能连通图的所有顶点而又不产生回路的任何子图都是它的生成树。
图中的极大连通子图称为连通分量。
在有向图G中,任意两点间都存在路径,则称G是强连通图。有向图中的极大强连通子图称做有向图的强连通分量。
连通图的生成树是一个极小的连通子图,它含有图中全部的n个顶点,但只有足以构成一棵树的n-1条边。比如下图的图1是普通图,当去掉两条构成环的边后,如图2或图3,就是一棵生成树。
从这里也可知道,如果一个图有n个顶点和小于n-1条边,则是非连通图,如果它多于n-1边条,必定构成一个环,因为这条边使得它依附的那两个顶点之间有了第二条路径。比如图2和图3,随便加哪两顶点的边都将构成环。不过有n-1条边并不一定是生成树,比如图4。
如果一个有向图有一个顶点的入度为0,其余顶点的入度均为1,则是一个有向树。入度为0相当于树中的根结点,其余顶点入度为1就是说树的非根结点的双亲只有一个。一个有向图的生成森林由若干棵有向树组成,含有图中全部顶点,但只有足以构成若干棵不相交的有向树的弧。如下图的图1是有向图。去掉一些弧后,它可以分解为两棵有向树,如图2和图3,这两棵就是图1有向图的生成森林。
图的存储结构
图不可能用简单的顺序存储结构来表示。而多重链表的方式,会造成很多存储单元的浪费。下面提供图的5种存储结构
邻接矩阵
图的邻接矩阵(Adjacency Matrix)存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。
设图G有n个顶点,则邻接矩阵是一个n×n的方阵,定义为:
无向图:
对于矩阵的主对角线的值,全为0是因为不存在顶点到自身的边。
无向图是一个对称矩阵。
有向图:
网图:
∞表示一个计算机允许的、大于所有边上权值的值,也就是一个不可能的极限值。个别时候权值可能就是0,甚至有可能是负值。因此必须要用一个不可能的值来代表不存在。
邻接矩阵代码如下:
/*邻接矩阵创建*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#define MAXVEX 100
#define INFINITY 2000000000
typedef struct
{
char vexs[MAXVEX]; /* 顶点表 */
int arc[MAXVEX][MAXVEX];/* 邻接矩阵,边表 */
int numNodes, numEdges; /* 图中当前的顶点数和边数 */
}MGraph;
/* 建立无向网图的邻接矩阵表示 */
void CreateMGraph(MGraph *G)
{
int i,j,k,w;
printf("输入顶点数和边数:\n");
scanf("%d %d",&G->numNodes,&G->numEdges); /* 输入顶点数和边数 */
for(i = 0;i <G->numNodes;i++) /* 读入顶点信息,建立顶点表 */
{
getchar(); //空格 换行符
G->vexs[i]=getchar();
}
for(i = 0;i <G->numNodes;i++)
for(j = 0;j <G->numNodes;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]; /* 因为是无向图,矩阵对称 */
}
}
int main(void)
{
MGraph G;
CreateMGraph(&G);
system("pause");
return 0;
}
n个顶点和e条边的无向网图的创建,时间复杂度为O(n+n2+e)
邻接表
邻接矩阵对于边数相对顶点较少的图存在对存储空间的极大浪费
树存储结构有一种孩子表示法,将结点存入数组,并对结点的孩子进行链式存储,不管有多少孩子,也不会存在空间浪费问题。这个思路同样适用于图的存储。我们把这种数组与链表相结合的存储方法称为邻接表(Ad-jacency List)。
处理方法如下:
- 图中顶点用一个一维数组存储,当然,顶点也可以用单链表来存储,不过数组可以较容易地读取顶点信息,更加方便。另外,对于顶点数组中,每个数据元素还需要存储指向第一个邻接点的指针,以便于查找该顶点的边信息。
- 图中每个顶点vi的所有邻接点构成一个线性表,由于邻接点的个数不定,所以用单链表存储,无向图称为顶点vi的边表,有向图则称为顶点vi作为弧尾的出边表。
若是有向图,邻接表结构是类似的,以顶点为弧尾来存储边表,这样很容易就可以得到每个顶点的出度。但也有时为了便于确定顶点的入度或以顶点为弧头的弧,可以建立一个有向图的逆邻接表,即对每个顶点vi都建立一个链接为vi为弧头的表。
对于带权值的网图,可以在边表结点定义中再增加一个weight的数据域,存储权值信息即可
代码如下:
/*邻接表创建*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#define MAXVEX 100
typedef struct EdgeNode /* 边表结点 */
{
int adjvex; /* 邻接点域,存储该顶点对应的下标 */
int info; /* 存储权值*/
struct EdgeNode *next; /* 链域,指向下一个邻接点 */
}EdgeNode;
typedef struct VertexNode /* 顶点表结点 */
{
char data; /* 顶点域,存储顶点信息 */
EdgeNode *firstedge;/* 边表头指针 */
}VertexNode, AdjList[MAXVEX];
typedef struct
{
AdjList adjList; //一维数组
int numNodes,numEdges; /* 图中当前顶点数和边数 */
}GraphAdjList;
/* 建立图的邻接表结构 */
void CreateALGraph(GraphAdjList *G)
{
int i,j,k;
EdgeNode *e;
printf("输入顶点数和边数:\n");
scanf("%d,%d",&G->numNodes,&G->numEdges); /* 输入顶点数和边数 */
for(i = 0;i < G->numNodes;i++) /* 建立顶点表 */
{
getchar();
scanf("%c",&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);
e=(EdgeNode *)malloc(sizeof(EdgeNode)); /* 生成边表结点 */
e->adjvex=j; /* 邻接序号为j */
e->next=G->adjList[i].firstedge; /* 将e的指针指向当前顶点上指向的结点 */
G->adjList[i].firstedge=e; /* 将当前顶点的指针指向e */
e=(EdgeNode *)malloc(sizeof(EdgeNode)); /* 无向图,对称 */
e->adjvex=i;
e->next=G->adjList[j].firstedge;
G->adjList[j].firstedge=e;
}
}
int main(void)
{
GraphAdjList G;
CreateALGraph(&G);
system("pause");
return 0;
}
本算法的时间复杂度对于n个顶点e条边来说是 O(n+e)
十字链表
对于有向图来说,邻接表是有缺陷的。关心了出度问题,想了解入度就必须要遍历整个图才能知道,反之,逆邻接表解决了入度却不了解出度的情况。
把它们整合在一起。这就是十字链表(Orthogonal List)。
重新定义的边表结点结构如表。
tailvex是弧起点在顶点表的下标,headvex是弧终点在顶点表中的下标,headlink是入边表指针域,指向终点相同的下一条边,taillink是边表指针域,指向起点相同的下一条边。虚线箭头就是此图的逆邻接表的表示。出入呈十字交叉
十字链表的把邻接表和逆邻接表整合在了一起,既容易找到以vi为尾的弧,也容易找到以vi为头的弧,因而容易求得顶点的出度和入度。除了结构复杂外,创建图算法的时间复杂度是和邻接表相同
邻接多重表
如果我们在无向图的应用中,关注的重点是顶点,那么邻接表是不错的选择,但如果更关注边的操作,比如对已访问过的边做标记,删除某一条边等操作,那就意味着,需要找到这条边的两个边表结点进行操作,这其实还是比较麻烦的。
对边表结点的结构进行一些改造,重新定义的边表结点结构如下所示。
ivex和jvex是与某条边依附的两个顶点在顶点表中的下标。ilink指向依附顶点ivex的下一条边,jlink指向依附顶点jvex的下一条边。这就是邻接多重表结构。
邻接多重表与邻接表的差别,仅仅是在于同一条边在邻接表中用两个结点表示,而在邻接多重表中只有一个结点。这样对边的操作就方便多了,若要删除左图的(v0,v2)这条边,只需要将右图的⑥⑨的链接指向改为∧即可。
边集数组
边集数组是由两个一维数组构成。此它更适合对边依次进行处理的操作,而不适合对顶点相关的操作。
图的遍历
BFS:
/*邻接矩阵DFS_BFS*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#define MAXSIZE 9
#define MAXEDGE 15
#define MAXVEX 9
#define INFINITY 2000000000
typedef struct
{
char vexs[MAXVEX];
int arc[MAXVEX][MAXVEX];
int numVertexes, numEdges;
}MGraph;
/* 循环队列的顺序存储结构 */
typedef struct
{
int data[MAXSIZE];
int front; /* 头指针 */
int rear; /* 尾指针,若队列不空,指向队列尾元素的下一个位置 */
}Queue;
/* 初始化一个空队列Q */
int InitQueue(Queue *Q)
{
Q->front=0;
Q->rear=0;
return 1;
}
/* 若队列Q为空队列,则返回TRUE,否则返回FALSE */
int QueueEmpty(Queue Q)
{
if(Q.front==Q.rear) /* 队列空的标志 */
return 1;
else
return 0;
}
/* 若队列未满,则插入元素e为Q新的队尾元素 */
int EnQueue(Queue *Q,int e)
{
if ((Q->rear+1)%MAXSIZE == Q->front) /* 队列满的判断 */
return 0;
Q->data[Q->rear]=e; /* 将元素e赋值给队尾 */
Q->rear=(Q->rear+1)%MAXSIZE;/* rear指针向后移一位置, */
/* 若到最后则转到数组头部 */
return 1;
}
/* 若队列不空,则删除Q中队头元素,用e返回其值 */
int DeQueue(Queue *Q,int *e)
{
if (Q->front == Q->rear) /* 队列空的判断 */
return 0;
*e=Q->data[Q->front]; /* 将队头元素赋值给e */
Q->front=(Q->front+1)%MAXSIZE; /* front指针向后移一位置, */
/* 若到最后则转到数组头部 */
return 1;
}
void CreateMGraph(MGraph *G)
{
int i, j;
G->numEdges=15;
G->numVertexes=9;
/* 读入顶点信息,建立顶点表 */
G->vexs[0]='A';
G->vexs[1]='B';
G->vexs[2]='C';
G->vexs[3]='D';
G->vexs[4]='E';
G->vexs[5]='F';
G->vexs[6]='G';
G->vexs[7]='H';
G->vexs[8]='I';
for (i = 0; i < G->numVertexes; i++)
{
for ( j = 0; j < G->numVertexes; j++)
{
G->arc[i][j]=0;
}
}
G->arc[0][1]=1;
G->arc[0][5]=1;
G->arc[1][2]=1;
G->arc[1][8]=1;
G->arc[1][6]=1;
G->arc[2][3]=1;
G->arc[2][8]=1;
G->arc[3][4]=1;
G->arc[3][7]=1;
G->arc[3][6]=1;
G->arc[3][8]=1;
G->arc[4][5]=1;
G->arc[4][7]=1;
G->arc[5][6]=1;
G->arc[6][7]=1;
for(i = 0; i < G->numVertexes; i++)
{
for(j = i; j < G->numVertexes; j++)
{
G->arc[j][i] =G->arc[i][j];
}
}
}
int visited[MAXVEX]; /* 访问标志的数组 */
/* 邻接矩阵的深度优先递归算法 */
void DFS(MGraph G, int i)
{
int j;
visited[i] = 1;
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] = 0; /* 初始所有顶点状态都是未访问过状态 */
for(i = 0; i < G.numVertexes; i++)
if(!visited[i]) /* 对未访问过的顶点调用DFS,若是连通图,只会执行一次 */
DFS(G, i);
}
/* 邻接矩阵的广度遍历算法 */
void BFSTraverse(MGraph G)
{
int i, j;
Queue Q;
for(i = 0; i < G.numVertexes; i++)
visited[i] = 0;
InitQueue(&Q); /* 初始化一辅助用的队列 */
for(i = 0; i < G.numVertexes; i++) /* 对每一个顶点做循环 */
{
if (!visited[i]) /* 若是未访问过就处理 */
{
visited[i]=1; /* 设置当前顶点访问过 */
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]=1; /* 将找到的此顶点标记为已访问 */
printf("%c ", G.vexs[j]); /* 打印顶点 */
EnQueue(&Q,j); /* 将找到的此顶点入队列 */
}
}
}
}
}
}
int main(void)
{
MGraph G;
CreateMGraph(&G);
printf("\n深度遍历:");
DFSTraverse(G);
printf("\n广度遍历:");
BFSTraverse(G);
system("pause");
return 0;
}
/*邻接表DFS_BFS*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#define MAXSIZE 9
#define MAXEDGE 15
#define MAXVEX 9
#define INFINITY 65535
/* 邻接矩阵结构 */
typedef struct
{
char vexs[MAXVEX]; /* 顶点表 */
int arc[MAXVEX][MAXVEX];/* 邻接矩阵,可看作边表 */
int numVertexes, numEdges; /* 图中当前的顶点数和边数 */
}MGraph;
/* 邻接表结构 */
typedef struct EdgeNode /* 边表结点 */
{
int adjvex; /* 邻接点域,存储该顶点对应的下标 */
int weight; /* 用于存储权值,对于非网图可以不需要 */
struct EdgeNode *next; /* 链域,指向下一个邻接点 */
}EdgeNode;
typedef struct VertexNode /* 顶点表结点 */
{
int in; /* 顶点入度 */
char data; /* 顶点域,存储顶点信息 */
EdgeNode *firstedge;/* 边表头指针 */
}VertexNode, AdjList[MAXVEX];
typedef struct
{
AdjList adjList;
int numVertexes,numEdges; /* 图中当前顶点数和边数 */
}graphAdjList,*GraphAdjList;
/* **************************** */
/* 循环队列的顺序存储结构 */
typedef struct
{
int data[MAXSIZE];
int front; /* 头指针 */
int rear; /* 尾指针,若队列不空,指向队列尾元素的下一个位置 */
}Queue;
/* 初始化一个空队列Q */
int InitQueue(Queue *Q)
{
Q->front=0;
Q->rear=0;
return 1;
}
/* 若队列Q为空队列,则返回TRUE,否则返回FALSE */
int QueueEmpty(Queue Q)
{
if(Q.front==Q.rear) /* 队列空的标志 */
return 1;
else
return 0;
}
/* 若队列未满,则插入元素e为Q新的队尾元素 */
int EnQueue(Queue *Q,int e)
{
if ((Q->rear+1)%MAXSIZE == Q->front) /* 队列满的判断 */
return 0;
Q->data[Q->rear]=e; /* 将元素e赋值给队尾 */
Q->rear=(Q->rear+1)%MAXSIZE;/* rear指针向后移一位置, */
/* 若到最后则转到数组头部 */
return 1;
}
/* 若队列不空,则删除Q中队头元素,用e返回其值 */
int DeQueue(Queue *Q,int *e)
{
if (Q->front == Q->rear) /* 队列空的判断 */
return 0;
*e=Q->data[Q->front]; /* 将队头元素赋值给e */
Q->front=(Q->front+1)%MAXSIZE; /* front指针向后移一位置, */
/* 若到最后则转到数组头部 */
return 1;
}
void CreateMGraph(MGraph *G)
{
int i, j;
G->numEdges=15;
G->numVertexes=9;
/* 读入顶点信息,建立顶点表 */
G->vexs[0]='A';
G->vexs[1]='B';
G->vexs[2]='C';
G->vexs[3]='D';
G->vexs[4]='E';
G->vexs[5]='F';
G->vexs[6]='G';
G->vexs[7]='H';
G->vexs[8]='I';
for (i = 0; i < G->numVertexes; i++)/* 初始化图 */
{
for ( j = 0; j < G->numVertexes; j++)
{
G->arc[i][j]=0;
}
}
G->arc[0][1]=1;
G->arc[0][5]=1;
G->arc[1][2]=1;
G->arc[1][8]=1;
G->arc[1][6]=1;
G->arc[2][3]=1;
G->arc[2][8]=1;
G->arc[3][4]=1;
G->arc[3][7]=1;
G->arc[3][6]=1;
G->arc[3][8]=1;
G->arc[4][5]=1;
G->arc[4][7]=1;
G->arc[5][6]=1;
G->arc[6][7]=1;
for(i = 0; i < G->numVertexes; i++)
{
for(j = i; j < G->numVertexes; j++)
{
G->arc[j][i] =G->arc[i][j];
}
}
}
/* 利用邻接矩阵构建邻接表 */
void CreateALGraph(MGraph G,GraphAdjList *GL)
{
int i,j;
EdgeNode *e;
*GL = (GraphAdjList)malloc(sizeof(graphAdjList));
(*GL)->numVertexes=G.numVertexes;
(*GL)->numEdges=G.numEdges;
for(i= 0;i <G.numVertexes;i++) /* 读入顶点信息,建立顶点表 */
{
(*GL)->adjList[i].in=0;
(*GL)->adjList[i].data=G.vexs[i];
(*GL)->adjList[i].firstedge=NULL; /* 将边表置为空表 */
}
for(i=0;i<G.numVertexes;i++) /* 建立边表 */
{
for(j=0;j<G.numVertexes;j++)
{
if (G.arc[i][j]==1)
{
e=(EdgeNode *)malloc(sizeof(EdgeNode));
e->adjvex=j; /* 邻接序号为j */
e->next=(*GL)->adjList[i].firstedge; /* 将当前顶点上的指向的结点指针赋值给e */
(*GL)->adjList[i].firstedge=e; /* 将当前顶点的指针指向e */
(*GL)->adjList[j].in++;
}
}
}
}
int visited[MAXSIZE]; /* 访问标志的数组 */
/* 邻接表的深度优先递归算法 */
void DFS(GraphAdjList GL, int i)
{
EdgeNode *p;
visited[i] = 1;
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] = 0; /* 初始所有顶点状态都是未访问过状态 */
for(i = 0; i < GL->numVertexes; i++)
if(!visited[i]) /* 对未访问过的顶点调用DFS,若是连通图,只会执行一次 */
DFS(GL, i);
}
/* 邻接表的广度遍历算法 */
void BFSTraverse(GraphAdjList GL)
{
int i;
EdgeNode *p;
Queue Q;
for(i = 0; i < GL->numVertexes; i++)
visited[i] = 0;
InitQueue(&Q);
for(i = 0; i < GL->numVertexes; i++)
{
if (!visited[i])
{
visited[i]=1;
printf("%c ",GL->adjList[i].data);/* 打印顶点,也可以其它操作 */
EnQueue(&Q,i);
while(!QueueEmpty(Q))
{
DeQueue(&Q,&i);
p = GL->adjList[i].firstedge; /* 找到当前顶点的边表链表头指针 */
while(p)
{
if(!visited[p->adjvex]) /* 若此顶点未被访问 */
{
visited[p->adjvex]=1;
printf("%c ",GL->adjList[p->adjvex].data);
EnQueue(&Q,p->adjvex); /* 将此顶点入队列 */
}
p = p->next; /* 指针指向下一个邻接点 */
}
}
}
}
}
int main(void)
{
MGraph G;
GraphAdjList GL;
CreateMGraph(&G);
CreateALGraph(G,&GL);
printf("\n深度遍历:");
DFSTraverse(GL);
printf("\n广度遍历:");
BFSTraverse(GL);
system("pause");
return 0;
}
最小生成树
把构造连通网的最小代价生成树称为最小生成树(Minimum Cost SpanningTree)。
求上图网结构的最小生成树
找连通网的最小生成树,经典的有两种算法,普里姆算法和克鲁斯卡尔算法。
普里姆(Prim)算法
从0号开始,从已生成的生成树的所有顶点相连的边中(不含可能使得树变成环的边,即只考虑当前生成树中的顶点与剩余不在树中顶点相连的边)选出权值最小的下一个点并入生成树,反复此步骤。
此算法的时间复杂度为O(n2),代码如下:
/*最小生成树_Prim*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#define MAXEDGE 20
#define MAXVEX 20
#define INFINITY 2000000000
typedef struct
{
int arc[MAXVEX][MAXVEX];
int numVertexes, numEdges;
}MGraph; //邻接矩阵
void CreateMGraph(MGraph *G) /* 构图 */
{
int i, j;
G->numEdges=15;
G->numVertexes=9;
for (i = 0; i < G->numVertexes; i++)
{
for ( j = 0; j < G->numVertexes; j++)
{
if (i==j)
G->arc[i][j]=0;
else
G->arc[i][j] = G->arc[j][i] = INFINITY;
}
}
G->arc[0][1]=10; G->arc[0][5]=11; G->arc[1][2]=18;
G->arc[1][8]=12; G->arc[1][6]=16; G->arc[2][8]=8;
G->arc[2][3]=22; G->arc[3][8]=21; G->arc[3][6]=24;
G->arc[3][7]=16; G->arc[3][4]=20; G->arc[4][7]=7;
G->arc[4][5]=26; G->arc[5][6]=17; G->arc[6][7]=19;
for(i = 0; i < G->numVertexes; i++)
{
for(j = i; j < G->numVertexes; j++)
{
G->arc[j][i] =G->arc[i][j];
}
}
}
/* Prim算法生成最小生成树 */
void MiniSpanTree_Prim(MGraph G)
{
int min, i, j, k;
int adjvex[MAXVEX]; /* 保存相关顶点前驱的下标 */
int lowcost[MAXVEX]; /* 保存相关顶点间边的权值 */
lowcost[0] = 0;/* 初始化第一个权值为0,即v0加入生成树 */
/*lowcost是对于全图的数组,重复使用,表示当前已生成的树到每个下标值点的最小权值*/
/* 凡是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; /* 初始化最小权值为∞ */
j = 1;k = 0;
while(j < G.numVertexes) /* 遍历全部顶点的lowcost选出最小值 */
{
if(lowcost[j]!=0 && lowcost[j] < min)/* 找出最小权值min */
{
min = lowcost[j];
k = j;
}
j++;
}
printf("(%d, %d)\n", adjvex[k], k); /* 打印当前顶点边中权值最小的边 */
lowcost[k] = 0; /* 此顶点已经完成任务 */
for(j = 1; j < G.numVertexes; j++) /* 循环所有顶点 */
{
if(lowcost[j]!=0 && G.arc[k][j] < lowcost[j]) //更新lowcost
{
lowcost[j] = G.arc[k][j]; /* 存入当前树到j号点的最小权值 */
adjvex[j] = k; /* 将j号点的前驱k存入adjvex */
}
}
}
}
int main(void)
{
MGraph G;
CreateMGraph(&G);
MiniSpanTree_Prim(G);
system("pause");
return 0;
}
克鲁斯卡尔(Kruskal)算法
Prim算法是以某顶点为起点,逐步找各顶点上最小权值的边来构建最小生成树。
同样的思路,我们也可以直接就以边为目标去构建,因为权值是在边上,直接去找最小权值的边来构建生成树,只不过构建时要考虑是否会形成环路而已。此时我们就用到了图的存储结构中的边集数组结构。
边集数组结构的定义代码:
typedef struct
{
int begin;
int end;
int weight;
} Edge;
将下图的邻接矩阵通过程序边集数组,并且对它们按权值从小到大排序。
如上图所示。parent[2]=8表示v2与v8在一个集合中,因此v2也在边集合A中。再由parent[3]=7、parent[4]=7和parent[7]=0可知v3、v4、v7在另一个边集合B中。
当i=7时,调用Find函数,会传入参数edges[7].begin=5。此时parent[5]=8>0,所以f=8,再循环得parent[8]=6。因parent[6]=0返回n=6。传入参数edges[7].end=6得到m=6。此时n=m,不再打印,继续下一循环。因为边(v5,v6)使得边集合A形成了环路。因此不能将它纳入到最小生成树中。即如果这条边连接的两个节点不在同一个集合中,则增加这条边到图中。
/*最小生成树_Kruskal*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#define MAXEDGE 20
#define MAXVEX 20
#define INFINITY 2000000000
typedef struct
{
int arc[MAXVEX][MAXVEX];
int numVertexes, numEdges;
}MGraph;
typedef struct
{
int begin;
int end;
int weight;
}Edge;
/* 构建图 */
void CreateMGraph(MGraph *G)
{
int i, j;
G->numEdges=15;
G->numVertexes=9;
for (i = 0; i < G->numVertexes; i++)/* 初始化图 */
{
for ( j = 0; j < G->numVertexes; j++)
{
if (i==j)
G->arc[i][j]=0;
else
G->arc[i][j] = G->arc[j][i] = INFINITY;
}
}
G->arc[0][1]=10; G->arc[0][5]=11; G->arc[1][2]=18;
G->arc[1][8]=12; G->arc[1][6]=16; G->arc[2][8]=8;
G->arc[2][3]=22; G->arc[3][8]=21; G->arc[3][6]=24;
G->arc[3][7]=16; G->arc[3][4]=20; G->arc[4][7]=7;
G->arc[4][5]=26; G->arc[5][6]=17; G->arc[6][7]=19;
for(i = 0; i < G->numVertexes; i++)
{
for(j = i; j < G->numVertexes; j++)
{
G->arc[j][i] =G->arc[i][j];
}
}
}
/* 交换权值 以及头和尾 */
void Swapn(Edge *edges,int i, int j)
{
int temp;
temp = edges[i].begin;
edges[i].begin = edges[j].begin;
edges[j].begin = temp;
temp = edges[i].end;
edges[i].end = edges[j].end;
edges[j].end = temp;
temp = edges[i].weight;
edges[i].weight = edges[j].weight;
edges[j].weight = temp;
}
/* 对权值进行排序 */
void sort(Edge edges[],MGraph *G)
{
int i, j;
for ( i = 0; i < G->numEdges; i++)
{
for ( j = i + 1; j < G->numEdges; j++)
{
if (edges[i].weight > edges[j].weight)
{
Swapn(edges, i, j);
}
}
}
printf("权排序之后的为:\n");
for (i = 0; i < G->numEdges; i++)
{
printf("(%d, %d) %d\n", edges[i].begin, edges[i].end, edges[i].weight);
}
}
/* 查找连线顶点的尾部下标 */
/*并查集思想 把每次新加入树的结点作为所有结点的祖先
因为每一个独立的树中的元素都指向同一个祖先,每次调用函数时,
边起始点所在树中的祖先和终止点所在树的祖先如果相等,证明两点本在一棵树上
相连即会构成环路。如果两点祖先不同,让终止点所在树的祖先指向起始点所在树的祖先,
两棵树并成一棵树,树上所有的结点有共同的祖先*/
int Find(int *parent, int f)
{
while ( parent[f] > 0)
{
f = parent[f];
}
return f;
}
/* 生成最小生成树 */
void MiniSpanTree_Kruskal(MGraph G)
{
int i, j, n, m;
int k = 0;
int parent[MAXVEX];/* 定义一数组用来判断边与边是否形成环路 */
//当parent[0]]=1,表示v0和v1已经在生成树的边集合中
Edge edges[MAXEDGE];/* 定义边集数组 */
/* 用来构建边集数组并排序 */
for ( i = 0; i < G.numVertexes-1; i++)
{
for (j = i + 1; j < G.numVertexes; j++)
{
if (G.arc[i][j]<INFINITY)
{
edges[k].begin = i;
edges[k].end = j;
edges[k].weight = G.arc[i][j];
k++;
}
}
}
sort(edges, &G);
for (i = 0; i < G.numVertexes; i++)
parent[i] = 0; /* 初始化数组值为0 */
printf("打印最小生成树:\n");
for (i = 0; i < G.numEdges; i++) /* 循环每一条边 */
{
n = Find(parent,edges[i].begin);
m = Find(parent,edges[i].end);
if (n != m) /* 不等说明边的两个顶点不在一个集合中,不构成环路 */
{
parent[n] = m; /* 将此边的结尾顶点放入下标为起点的parent中, */
/* 表示此顶点已经在生成树集合中 */
printf("(%d, %d) %d\n", edges[i].begin, edges[i].end, edges[i].weight);
}
}
}
int main()
{
MGraph G;
CreateMGraph(&G);
MiniSpanTree_Kruskal(G);
system("pause");
return 0;
}
最短路径
在网图和非网图中,最短路径的含义是不同的。由于非网图它没有边上的权值
最小生成树能够保证整个拓扑图的所有路径之和最小,但不能保证任意两点之间是最短路径。
最短路径是从一点出发,到达目的地的路径最小。
最小生成树构成后所有的点都被连通,而最短路只要到达目的地走的是最短的路径即可,与所有的点连不连通没有关系。
迪杰斯特拉(Dijkstra)算法
Dijkstra算法其实是改进后的BFS
从未访问过的离起点最近的点开始,每次计算当前结点到它未访问过的邻居结点的的距离,每次更新到邻居结点的最小值
/*最短路径_Dijkstra*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#define MAXEDGE 20
#define MAXVEX 20
#define INFINITY 2000000000
typedef struct
{
int vexs[MAXVEX];
int arc[MAXVEX][MAXVEX];
int numVertexes, numEdges;
}MGraph;
typedef int Patharc[MAXVEX]; /* 用于存储最短路径下标的数组 */
typedef int ShortPathTable[MAXVEX];/* 用于存储到各点最短路径的权值和 */
/* 构建图的邻接矩阵 */
void CreateMGraph(MGraph *G)
{
int i, j;
G->numEdges=16;
G->numVertexes=9;
for (i = 0; i < G->numVertexes; i++)
{
G->vexs[i]=i;
}
for (i = 0; i < G->numVertexes; i++)
{
for ( j = 0; j < G->numVertexes; j++)
{
if (i==j)
G->arc[i][j]=0;
else
G->arc[i][j] = G->arc[j][i] = INFINITY;
}
}
G->arc[0][1]=1;G->arc[0][2]=5;G->arc[1][2]=3;G->arc[1][3]=7;
G->arc[1][4]=5;G->arc[2][4]=1;G->arc[2][5]=7;G->arc[3][4]=2;
G->arc[3][6]=3;G->arc[4][5]=3;G->arc[4][6]=6;G->arc[4][7]=9;
G->arc[5][7]=5;G->arc[6][7]=2;G->arc[6][8]=7;G->arc[7][8]=4;
for(i = 0; i < G->numVertexes; i++)
{
for(j = i; j < G->numVertexes; j++)
{
G->arc[j][i] =G->arc[i][j];
}
}
}
/* Dijkstra算法,求有向网G的v0顶点到其余顶点v的最短路径P[v]及带权长度D[v] */
/* P[v]的值为前驱顶点下标,D[v]表示v0到v的最短路径长度和 */
void ShortestPath_Dijkstra(MGraph G, int v0, Patharc *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.arc[v0][v];/* 将与v0点有连线的顶点加上权值 */
(*P)[v] = -1; /* 初始化路径数组P为-1 */
}
(*D)[v0] = 0; /* v0至v0路径为0 */
final[v0] = 1;
/* 循环更新v0到每个顶点的最短路径 */
for(v=1; v<G.numVertexes; v++)
{
min=INFINITY; /* 当前所知离v0顶点的最近距离 */
for(w=0; w<G.numVertexes; w++) /* 每次挑出离当前点最近的一个邻居顶点并访问 */
{
if(final[w]==0 && (*D)[w]<min)
{
k=w;
min = (*D)[w]; /* w顶点离v0顶点更近 */
}
}
final[k] = 1; /* 将目前找到的最近的顶点置为1,表示该点已访问 */
for(w=0; w<G.numVertexes; w++) /* 修正当前最短路径及距离 */
{
/* 如果存在比直达更短的路 */
if(final[w]==0 && (min+G.arc[k][w]<(*D)[w]))
//D存储的是源点直达距离,此处和由前驱到达此点的中转路径长比较
{ //如果中转的方式更短 则更新中转到此点的前驱信息 否则前驱=-1即从源点直达此点
(*D)[w] = min + G.arc[k][w];
(*P)[w]=k;
}
}
}
}
int main()
{
int i,j,v0;
MGraph G;
Patharc P; //存储最短路径下标(中转点下标)的数组
ShortPathTable D; //存储到各点最短路径的权值和的数组
v0=0; //源点值
CreateMGraph(&G);
ShortestPath_Dijkstra(G, v0, &P, &D);
printf("最短路径中转点倒序如下:\n"); //如果直达最短,则无中转点,不需要输出中转点
for(i=1;i<G.numVertexes;++i)
{
printf("v%d - v%d : ",v0,i);
j=i;
while(P[j]!=-1)
{
printf("%d ",P[j]);
j=P[j];
}
printf("\n");
}
printf("\n源点到各顶点的最短路径长度为:\n");
for(i=1;i<G.numVertexes;++i)
printf("v%d - v%d : %d \n",v0,G.vexs[i],D[i]);
printf("\n0-8数组P的值如下:\n");
for(int i=0;i<=8;i++)
{
printf("P[%d]=%d\n",i,P[i]);
}
system("pause");
return 0;
}
O(n3)
弗洛伊德(Floyd)算法
对于每个顶点v,和任一顶点对(i,j) , i≠j,v≠i,v≠j,
如果 A[i][j] > A[i][v]+A[v][j]
,则将A[i][j]
更新为A[i][v]+A[v][j]
的值
并且将Path[i][j]
改为v
代码如下:
/*最短路径_Floyd*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#define MAXEDGE 20
#define MAXVEX 20
#define INFINITY 200000000
typedef struct
{
int vexs[MAXVEX];
int arc[MAXVEX][MAXVEX];
int numVertexes, numEdges;
}MGraph;
typedef int Patharc[MAXVEX][MAXVEX];
typedef int ShortPathTable[MAXVEX][MAXVEX];
/* 构建图的邻接矩阵 */
void CreateMGraph(MGraph *G)
{
int i, j;
G->numEdges=16;
G->numVertexes=9;
for (i = 0; i < G->numVertexes; i++)
{
G->vexs[i]=i;
}
for (i = 0; i < G->numVertexes; i++)
{
for ( j = 0; j < G->numVertexes; j++)
{
if (i==j)
G->arc[i][j]=0;
else
G->arc[i][j] = G->arc[j][i] = INFINITY;
}
}
G->arc[0][1]=1;G->arc[0][2]=5;G->arc[1][2]=3;G->arc[1][3]=7;
G->arc[1][4]=5;G->arc[2][4]=1;G->arc[2][5]=7;G->arc[3][4]=2;
G->arc[3][6]=3;G->arc[4][5]=3;G->arc[4][6]=6;G->arc[4][7]=9;
G->arc[5][7]=5;G->arc[6][7]=2;G->arc[6][8]=7;G->arc[7][8]=4;
for(i = 0; i < G->numVertexes; i++)
{
for(j = i; j < G->numVertexes; j++)
{
G->arc[j][i] =G->arc[i][j];
}
}
}
/* Floyd算法,求网图G中各顶点v到其余顶点w的最短路径P[v][w]及带权长度D[v][w]。 */
void ShortestPath_Floyd(MGraph G, Patharc *P, ShortPathTable *D)
{
int v,w,k;
for(v=0; v<G.numVertexes; v++) /* 初始化D与P */
{
for(w=0; w<G.numVertexes; w++)
{
(*D)[v][w]=G.arc[v][w]; /* D[v][w]值即为对应点间的权值 */
(*P)[v][w]=w; /* 初始化P */
}
}
for(k=0; k<G.numVertexes; k++)//第一层循环选出所有的中间点,中间两层循环选出所有的顶点对
{
for(v=0; v<G.numVertexes; v++)
{
for(w=0; w<G.numVertexes; w++)
{
if ((*D)[v][w]>(*D)[v][k]+(*D)[k][w])
{/* 如果经过下标为k顶点路径比原两点间路径更短 */
(*D)[v][w]=(*D)[v][k]+(*D)[k][w];/* 将当前两点间权值设为更小的一个 */
(*P)[v][w]=(*P)[v][k];/* 路径设置为经过下标为k的顶点 */
}
}
}
}
}
int main()
{
int v,w,k;
MGraph G;
Patharc P;
ShortPathTable D; /* 求某点到其余各点的最短路径 */
CreateMGraph(&G);
ShortestPath_Floyd(G,&P,&D);
printf("各顶点间最短路径如下:\n");
for(v=0; v<G.numVertexes; v++)
{
for(w=v+1; w<G.numVertexes; w++)
{
printf("v%d-v%d weight: %d ",v,w,D[v][w]);
k=P[v][w]; /* 获得第一个路径顶点下标 */
printf(" path: %d",v); /* 打印源点 */
while(k!=w) /* 如果路径顶点下标不是终点 */
{
printf(" -> %d",k); /* 打印路径顶点 */
k=P[k][w]; /* 获得下一个路径顶点下标 */
}
printf(" -> %d\n",w); /* 打印终点 */
}
printf("\n");
}
printf("最短路径D\n");
for(v=0; v<G.numVertexes; ++v)
{
for(w=0; w<G.numVertexes; ++w)
{
printf("%d\t",D[v][w]);
}
printf("\n");
}
printf("最短路径P\n");
for(v=0; v<G.numVertexes; ++v)
{
for(w=0; w<G.numVertexes; ++w)
{
printf("%d ",P[v][w]);
}
printf("\n");
}
system("pause");
return 0;
}
拓扑排序
有向图为顶点表示活动的网,称为AOV网(ActivityOn Vertex Network)。AOV网中的弧表示活动之间存在的某种制约关系。
设G=(V,E)
是一个具有n个顶点的有向图,V中的顶点序列v1,v2,……,vn
,满足若从顶点vi
到vj
有一条路径,则在顶点序列中顶点vi
必在顶点vj
之前。则我们称这样的顶点序列为一个拓扑序列。
上图这样的AOV网的拓扑序列不止一条。序列v0v1v2v3v4v5v6v7v8v9v10v11v12v13v14v15v16
是一条拓扑序列,而v0v1v4v3v2v7v6v5v8v10v9v12v11v14v13v15v16
也是一条拓扑序列。
拓扑排序就是对一个有向图构造拓扑序列的过程。
如果此网的全部顶点都被输出,则说明它是不存在环的AOV网;
如果输出顶点数少了,说明这个网存在环,不是AOV网。
一个不存在回路的AOV网,可以应用在各种各样的工程或项目的流程图中,满足各种应用场景的需要,所以实现拓扑排序的算法很有价值。
对AOV网进行拓扑排序的基本思路是:从AOV网中选择一个入度为0的顶点输出,然后删去此顶点,并删除以此顶点为尾的弧,继续重复此步骤,直到输出全部顶点或者AOV网中不存在入度为0的顶点为止。
由于拓扑排序的过程中,需要删除顶点,显然用邻接表会更加方便。
/*拓扑排序_TopologicalSort*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#define MAXEDGE 20
#define MAXVEX 14
#define INFINITY 2000000000
/* 邻接矩阵结构 */
typedef struct
{
int vexs[MAXVEX];
int arc[MAXVEX][MAXVEX];
int numVertexes, numEdges;
}MGraph;
/* 邻接表结构 */
typedef struct EdgeNode /* 边表结点 */
{
int adjvex; /* 邻接点域,存储该顶点对应的下标 */
//int weight; /* 权值 */
struct EdgeNode *next; /* 链域,指向下一个邻接点 */
}EdgeNode;
typedef struct VertexNode /* 顶点表结点 */
{
int in; /* 顶点入度 */
int data; /* 顶点域,存储顶点信息 */
EdgeNode *firstedge;/* 边表头指针 */
}VertexNode, AdjList[MAXVEX];
typedef struct
{
AdjList adjList;
int numVertexes,numEdges; /* 图中当前顶点数和边数 */
}graphAdjList,*GraphAdjList;
void CreateMGraph(MGraph *G)/* 构建图的邻接矩阵 */
{
int i, j;
G->numEdges=MAXEDGE;
G->numVertexes=MAXVEX;
for (i = 0; i < G->numVertexes; i++)
{
G->vexs[i]=i;
}
for (i = 0; i < G->numVertexes; i++)
{
for ( j = 0; j < G->numVertexes; j++)
{
G->arc[i][j]=0;
}
}
G->arc[0][4]=1; G->arc[0][5]=1; G->arc[0][11]=1; G->arc[1][2]=1;
G->arc[1][4]=1; G->arc[1][8]=1; G->arc[2][5]=1; G->arc[2][6]=1;
G->arc[2][9]=1; G->arc[3][2]=1; G->arc[3][13]=1; G->arc[4][7]=1;
G->arc[5][8]=1; G->arc[5][12]=1;G->arc[6][5]=1; G->arc[8][7]=1;
G->arc[9][10]=1;G->arc[9][11]=1;G->arc[10][13]=1;G->arc[12][9]=1;
}
/* 利用邻接矩阵构建邻接表 */
void CreateALGraph(MGraph G,GraphAdjList *GL)
{
int i,j;
EdgeNode *e;
*GL = (GraphAdjList)malloc(sizeof(graphAdjList));
(*GL)->numVertexes=G.numVertexes;
(*GL)->numEdges=G.numEdges;
for(i= 0;i <G.numVertexes;i++) /* 读入顶点信息,建立顶点表 */
{
(*GL)->adjList[i].in=0;
(*GL)->adjList[i].data=G.vexs[i];
(*GL)->adjList[i].firstedge=NULL; /* 将边表置为空表 */
}
for(i=0;i<G.numVertexes;i++) /* 建立边表 */
{
for(j=0;j<G.numVertexes;j++)
{
if (G.arc[i][j]==1)
{
e=(EdgeNode *)malloc(sizeof(EdgeNode));
e->adjvex=j; /* 邻接序号为j */
e->next=(*GL)->adjList[i].firstedge; /* 将当前顶点表上的指针域赋值给e的后继指针域*/
(*GL)->adjList[i].firstedge=e; /* 将顶点i的指针指向e (实现e的头插) */
(*GL)->adjList[j].in++; //顶点j入度+1
}
}
}
}
/* 拓扑排序,若GL无回路,则输出拓扑排序序列并返回1,若有回路返回0。 */
int TopologicalSort(GraphAdjList GL)
{
EdgeNode *e;
int i,k,gettop;
int top=0; /* 用于栈指针下标 */
int count=0;/* 用于统计输出顶点的个数 */
int *stack; /* 建栈,将入度为0的顶点入栈 */
stack=(int *)malloc(GL->numVertexes * sizeof(int) );
for(i = 0; i<GL->numVertexes; i++)
if(GL->adjList[i].in == 0) /* 将入度为0的顶点入栈 */
stack[++top]=i;
while(top!=0)
{
gettop=stack[top--];
printf("%d -> ",GL->adjList[gettop].data);
count++;
for(e = GL->adjList[gettop].firstedge; e; e = e->next)
{
k=e->adjvex;
if( (--GL->adjList[k].in) == 0 ) /* 将i号顶点的邻接点的入度减1,如果减1后为0,则入栈 */
stack[++top]=k;
}
}
printf("\n");
if(count < GL->numVertexes)
return 0;
else
return 1;
}
int main()
{
MGraph G;
GraphAdjList GL;
int result;
CreateMGraph(&G);
CreateALGraph(G,&GL);
result=TopologicalSort(GL);
printf("result:%d\n",result);
system("pause");
return 0;
}
对一个具有n个顶点e条弧的AOV网来说,将入度为0的顶点入栈的时间复杂为O(n),之后的while循环中,每个顶点进一次栈,出一次栈,入度减1的操作共执行了e次,所以整个算法的时间复杂度为O(n+e)
关键路径
在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间,这种有向图的边表示活动的网,称之为AOE网(Activity On Edge Net-work)。
把AOE网中没有入边的顶点称为始点或源点,没有出边的顶点称为终点或汇点。由于一个工程,总有一个开始,一个结束,所以正常情况下,AOE网只有一个源点一个汇点。
下图就是一个AOE网。其中v0
即是源点,v9
是汇点,顶点v0,v1,……,v9
分别表示事件,弧<v0,v1>,<v0,v2>,……,<v8,v9>
都表示一个活动,用a0,a1,……,a12
表示活动持续的时间。
AOE网是要建立在活动之间制约关系没有矛盾的基础之上,再来分析完成整个工程至少需要多少时间,或者为缩短完成工程所需时间,应当加快哪些活动.
把路径上各个活动所持续的时间之和称为路径长度,从源点到汇点具有最大长度的路径叫关键路径,在关键路径上的活动叫关键活动。
AOE网的性质:
⑴ 只有在某顶点所代表的事件发生后,从该顶点出发的各活动才能开始;
⑵ 只有在进入某顶点的各活动都结束,该顶点所代表的事件才能发生。
与关键活动有关的量:
⑴ 事件的最早发生时间ve[k] etv(earliest time ofvertex)
ve[k]是指从始点开始到顶点vk的最大路径长度。这个长度决定了所有从顶点vk发出的活动能够开工的最早时间。
⑵ 事件的最迟发生时间vl[k] ltv(latest time ofvertex)
vl[k]是指在不推迟整个工期的前提下,事件vk允许的最晚发生时间。
⑶ 活动的最早开始时间e[i] ete(earliest time ofedge)
若活动ai是由弧<vk , vj>表示,则活动ai的最早开始时间应等于事件vk的最早发生时间。因此,有:e[i]=ve[k]
⑷ 活动的最晚开始时间l[i] lte(latest time ofedge)
活动ai的最晚开始时间是指,在不推迟整个工期的前提下, ai必须开始的最晚时间。若ai由弧<vk,vj>表示,则ai的最晚开始时间要保证事件vj的最迟发生时间不拖后。因此,有:l[i]=vl[j]-len<vk,vj>
算法思路:只需要找到所有活动的最早开始时间和最晚开始时间,并且比较它们,如果相等就意味着此活动是关键活动,活动间的路径为关键路径。如果不等,则就不是。
AOE网转化为邻接表结构如下图
求事件的最早发生时间etv的过程,就是从头至尾找拓扑序列的过程,因此,在求关键路径之前,需要先调用一次拓扑序列算法的代码来计算etv和拓扑序列列表。执行完毕后,全局变量数组etv和栈stack的值如下图所示,对于每个事件的最早发生时间,已经计算出来了。
计算ltv,是倒着来的
代码如下:
/*关键路径_CriticalPath*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#define MAXEDGE 30
#define MAXVEX 30
#define INFINITY 2000000000
int *etv,*ltv; /* 事件最早发生时间和最迟发生时间数组,全局变量 */
int *stack2; /* 用于存储拓扑序列的栈 */
int top2; /* 用于stack2的指针 */
/* 邻接矩阵结构 */
typedef struct
{
int vexs[MAXVEX];
int arc[MAXVEX][MAXVEX];
int numVertexes, numEdges;
}MGraph;
/* 邻接表结构 */
typedef struct EdgeNode /* 边表结点 */
{
int adjvex; /* 邻接点域,存储该顶点对应的下标 */
int weight; /* 用于存储权值,对于非网图可以不需要 */
struct EdgeNode *next; /* 链域,指向下一个邻接点 */
}EdgeNode;
typedef struct VertexNode /* 顶点表结点 */
{
int in; /* 顶点入度 */
int data; /* 顶点域,存储顶点信息 */
EdgeNode *firstedge;/* 边表头指针 */
}VertexNode, AdjList[MAXVEX];
typedef struct
{
AdjList adjList;
int numVertexes,numEdges; /* 图中当前顶点数和边数 */
}graphAdjList,*GraphAdjList;
void CreateMGraph(MGraph *G)/* 构建图的邻接矩阵 */
{
int i, j;
G->numEdges=13;
G->numVertexes=10;
for (i = 0; i < G->numVertexes; i++)
{
G->vexs[i]=i;
}
for (i = 0; i < G->numVertexes; i++)
{
for ( j = 0; j < G->numVertexes; j++)
{
if (i==j)
G->arc[i][j]=0;
else
G->arc[i][j]=INFINITY;
}
}
G->arc[0][1]=3;G->arc[0][2]=4; G->arc[1][3]=5;G->arc[1][4]=6;
G->arc[2][3]=8;G->arc[2][5]=7; G->arc[3][4]=3;G->arc[4][6]=9;
G->arc[4][7]=4;G->arc[5][7]=6; G->arc[6][9]=2;G->arc[7][8]=5;
G->arc[8][9]=3;
}
/* 利用邻接矩阵构建邻接表 */
void CreateALGraph(MGraph G,GraphAdjList *GL)
{
int i,j;
EdgeNode *e;
*GL = (GraphAdjList)malloc(sizeof(graphAdjList));
(*GL)->numVertexes=G.numVertexes;
(*GL)->numEdges=G.numEdges;
for(i= 0;i <G.numVertexes;i++) /* 读入顶点信息,建立顶点表 */
{
(*GL)->adjList[i].in=0;
(*GL)->adjList[i].data=G.vexs[i];
(*GL)->adjList[i].firstedge=NULL; /* 将边表置为空表 */
}
for(i=0;i<G.numVertexes;i++) /* 建立边表 */
{
for(j=0;j<G.numVertexes;j++)
{
if (G.arc[i][j]!=0 && G.arc[i][j]<INFINITY)
{
e=(EdgeNode *)malloc(sizeof(EdgeNode));
e->adjvex=j; /* 邻接序号为j */
e->weight=G.arc[i][j];
e->next=(*GL)->adjList[i].firstedge; /* 将当前顶点上的指向的结点指针赋值给e */
(*GL)->adjList[i].firstedge=e; /* 将当前顶点的指针指向e */
(*GL)->adjList[j].in++;
}
}
}
}
/* 拓扑排序 若GL无回路,则输出拓扑排序序列并返回1,若有回路返回0。*/
int TopologicalSort(GraphAdjList GL)
{
EdgeNode *e;
int i,k,gettop;
int top=0; /* 用于栈指针下标 */
int count=0;/* 用于统计输出顶点的个数 */
int *stack; /* 建栈将入度为0的顶点入栈 */
stack=(int *)malloc(GL->numVertexes * sizeof(int) );
for(i = 0; i<GL->numVertexes; i++)
if(0 == GL->adjList[i].in) /* 将入度为0的顶点入栈 */
stack[++top]=i;
top2=0;
etv=(int *)malloc(GL->numVertexes * sizeof(int) ); /* 事件最早发生时间数组 */
for(i=0; i<GL->numVertexes; i++)
etv[i]=0; /* 初始化etv */
stack2=(int *)malloc(GL->numVertexes * sizeof(int) );/* 初始化拓扑序列栈 */
printf("TopologicalSort: ");
while(top!=0)
{
gettop=stack[top--];
printf("%d -> ",GL->adjList[gettop].data);
count++; /* 输出i号顶点,并计数 */
stack2[++top2]=gettop; /* 将弹出的顶点序号压入拓扑序列的栈stack2 */
for(e = GL->adjList[gettop].firstedge; e; e = e->next) //遍历每个子节点e
{
k=e->adjvex;
if( (--GL->adjList[k].in) == 0 ) /* 将i号顶点的邻接点的入度减1,如果减1后为0,则入栈 */
stack[++top]=k;
if((etv[gettop] + e->weight)>etv[k]) /* 求各顶点事件的最早发生时间etv值 */
etv[k] = etv[gettop] + e->weight;
}
}
printf("\n");
if(count < GL->numVertexes)
return 0;
else
return 1;
}
/* 求关键路径,GL为有向网,输出G的各项关键活动 */
void CriticalPath(GraphAdjList GL)
{
EdgeNode *e;
int i,gettop,k,j;
int ete,lte; /* 声明活动最早发生时间和最迟发生时间变量 */
TopologicalSort(GL); /* 求拓扑序列,计算数组etv和stack2的值 */
ltv=(int *)malloc(GL->numVertexes*sizeof(int));/* 事件最早发生时间数组 */
for(i=0; i<GL->numVertexes; i++)
ltv[i]=etv[GL->numVertexes-1]; /* 初始化 */
printf("etv: ");
for(i=0; i<GL->numVertexes; i++)
printf("%d -> ",etv[i]);
printf("\n");
while(top2!=0) /* 出栈是求ltv */
{
gettop=stack2[top2--];
for(e = GL->adjList[gettop].firstedge; e; e = e->next) /* 求各顶点事件的最迟发生时间ltv值 */
{ //最后一个顶点没有子节点 跳过本循环 ltv=etv
//第一个顶点下标=0 也跳过本循环 ltv=etv
k=e->adjvex;
if(ltv[k] - e->weight < ltv[gettop])
ltv[gettop] = ltv[k] - e->weight;
}
}
printf("ltv: ");
for(i=0; i<GL->numVertexes; i++)
printf("%d -> ",ltv[i]);
printf("\n");
for(j=0; j<GL->numVertexes; j++) /* 求ete,lte和关键活动 */
{
for(e = GL->adjList[j].firstedge; e; e = e->next)
{
k=e->adjvex;
ete = etv[j]; /* 活动最早发生时间 */
lte = ltv[k] - e->weight; /* 活动最迟发生时间 */
if(ete == lte) /* 两者相等即在关键路径上 */
printf("关键活动 :<v%d - v%d> length: %d \n",
GL->adjList[j].data,GL->adjList[k].data,e->weight);
}
}
}
int main()
{
MGraph G;
GraphAdjList GL;
CreateMGraph(&G);
CreateALGraph(G,&GL);
CriticalPath(GL);
system("pause");
return 0;
}
查找
静态查找表(Static Search Table):主要操作有:
(1)查询某个“特定的”数据元素是否在查找表中。
(2)检索某个“特定的”数据元素和各种属性。
动态查找表(Dynamic Search Table):主要操作有:
(1)查找时插入数据元素。
(2)查找时删除数据元素。
顺序表查找
二分查找改进:插值查找
二分计算中值:
将这个1/2进行改进,改进为下面的计算方案:
假设a[11]={0,1,16,24,35,47,59,62,73,88,99}
,low=1,high=10
,则a[low]=1,a[high]=99
,如果要找的是key=16时,按原来折半的做法,需要四次才可以得到结果,但如果用新办法,(key-a[low])/(a[high]-a[low])=(16-1)/(99-1)≈0.153
,即mid≈1+0.153×(10-1)=2.377
取整得到mid=2,只需要二次查找,显然大大提高了效率。
斐波那契查找(Fibonacci Search),是利用了黄金分割原理来实现的。
斐波那契查找算法的核心在于:
1)当key=a[mid]时,查找就成功;
2)当key<a[mid]时,新范围是第low个到第mid-1个,此时范围个数为F[k-1]-1个;
3)当key>a[mid]时,新范围是第m+1个到第high个,此时范围个数为F[k-2]-1个。
也就是说,如果要查找的记录在右侧,则左侧的数据都不用再判断了,不断反复进行下去,对处于当中的大部分数据,其工作效率要高一些。所以尽管斐波那契查找的时间复杂也为O(logn),但就平均性能来说,斐波那契查找要优于折半查找。可惜如果是最坏情况,比如这里key=1,那么始终都处于左侧长半区在查找,则查找效率要低于折半查找。
还有比较关键的一点,折半查找是进行加法与除法运算(mid=(low+high)/2),插值查找进行复杂的四则运算(mid=low+(high-low)*(key-a[low])/(a[high]-a[low])),而斐波那契查找只是最简单加减法运算(mid=low+F[k-1]-1),在海量数据的查找过程中,这种细微的差别可能会影响最终的查找效率。
线性索引查找:
- 稠密索引:在线性索引中,将数据集中的每个记录对应一个索引项
- 分块索引:对于分块有序的数据集,将每块对应一个索引项
- 倒排索引:记录号表存储具有相同次关键字的所有记录的记录号
/*静态查找*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#define MAXSIZE 100 /* 存储空间初始分配量 */
int F[100]; /* 斐波那契数列 */
/* 无哨兵顺序查找,a为数组,n为要查找的数组个数,key为要查找的关键字 */
int Sequential_Search(int *a,int n,int key)
{
int i;
for(i=1;i<=n;i++)
{
if (a[i]==key)
return i;
}
return 0;
}
/* 有哨兵顺序查找(顺序查找优化) */
/*在查找方向的尽头放置“哨兵”免去了在查找过程中
每一次比较后都要判断查找位置是否越界,
看似与原先差别不大,但在总数据较多时,效率提高很大。
当然,“哨兵”也可以在末端。*/
int Sequential_Search2(int *a,int n,int key)
{
int i;
a[0]=key;
i=n;
while(a[i]!=key)
{
i--;
}
return i;
}
/* 二分查找 */
int Binary_Search(int *a,int n,int key)
{
int low,high,mid;
low=1; /* 定义最低下标为记录首位 */
high=n; /* 定义最高下标为记录末位 */
while(low<=high)
{
mid=(low+high)/2; /* 折半 */
if (key<a[mid]) /* 若查找值比中值小 */
high=mid-1; /* 最高下标调整到中位下标小一位 */
else if (key>a[mid])/* 若查找值比中值大 */
low=mid+1; /* 最低下标调整到中位下标大一位 */
else
{
return mid; /* 若相等则说明mid即为查找到的位置 */
}
}
return 0;
}
/* 插值查找 */
int Interpolation_Search(int *a,int n,int key)
{
int low,high,mid;
low=1; /* 定义最低下标为记录首位 */
high=n; /* 定义最高下标为记录末位 */
while(low<=high)
{
mid=low+ (high-low)*(key-a[low])/(a[high]-a[low]); /* 插值 */
if (key<a[mid]) /* 若查找值比插值小 */
high=mid-1; /* 最高下标调整到插值下标小一位 */
else if (key>a[mid])/* 若查找值比插值大 */
low=mid+1; /* 最低下标调整到插值下标大一位 */
else
return mid; /* 若相等则说明mid即为查找到的位置 */
}
return 0;
}
/* 斐波那契查找 */
int Fibonacci_Search(int *a,int n,int key)
{
int low,high,mid,i,k=0;
low=1; /* 定义最低下标为记录首位 */
high=n; /* 定义最高下标为记录末位 */
while(n>F[k]-1)
k++;
for (i=n;i<F[k]-1;i++)
a[i]=a[n];
while(low<=high)
{
mid=low+F[k-1]-1;
if (key<a[mid])
{
high=mid-1;
k=k-1;
}
else if (key>a[mid])
{
low=mid+1;
k=k-2;
}
else
{
if (mid<=n)
return mid; /* 若相等则说明mid即为查找到的位置 */
else
return n;
}
}
return 0;
}
int main()
{
int a[MAXSIZE+1],i,result;
int arr[MAXSIZE]={0,1,16,24,35,47,59,62,73,88,99};
for(i=0;i<=MAXSIZE;i++)
{
a[i]=i;
}
result=Sequential_Search(a,MAXSIZE,MAXSIZE);
printf("Sequential_Search:%d \n",result);
result=Sequential_Search2(a,MAXSIZE,1);
printf("Sequential_Search2:%d \n",result);
result=Binary_Search(arr,10,62);
printf("Binary_Search:%d \n",result);
result=Interpolation_Search(arr,10,62);
printf("Interpolation_Search:%d \n",result);
F[0]=0; F[1]=1;
for(i = 2;i < 100;i++)
{
F[i] = F[i-1] + F[i-2];
}
result=Fibonacci_Search(arr,10,62);
printf("Fibonacci_Search:%d \n",result);
system("pause");
return 0;
}
二叉排序树
一种即可以使得插入和删除效率不错,又可以比较高效率地实现查找的算法
当对它进行中序遍历时,就可以得到一个有序的序列{35,37,47,51,58,62,73,88,93,99}
,所以通常称它为二叉排序树
删除操作:
对这棵二叉排序树进行中序遍历,得到的序列{29,35,36,37,47,48,49,50,51,56,58,62,73,88,93,99}
,37,48正好是47的前驱和后继。
因此,比较好的办法就是,找到需要删除的结点p的直接前驱(或直接后继)s,用s来替换结点p,然后再删除此结点s
else /* 左右子树均不空 */
{
q=*p; s=(*p)->lchild;
while(s->rchild) /* 转左,然后向右到尽头(找待删结点的前驱) */
{
q=s;
s=s->rchild;
}
(*p)->data=s->data; /* s指向被删结点的直接前驱 */
if(q!=*p)
q->rchild=s->lchild; /* 重接q的右子树 */
else
q->lchild=s->lchild; /* 重接q的左子树 */
free(s);
}
关于上面代码的理解如下:
如果p和q指向不同,则将s->lchild赋值给q->rchild,否则就是将s->lchild赋值给q->lchild。 p,q指向相同时说明要删除的结点的左孩子没有孩子 如要删除35时p=q
/*二叉排序树_BinarySortTree*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#define MAXSIZE 100
/* 二叉树的二叉链表结点结构定义 */
typedef struct BiTNode /* 结点结构 */
{
int data; /* 结点数据 */
struct BiTNode *lchild, *rchild; /* 左右孩子指针 */
} BiTNode, *BiTree;
/* 递归查找二叉排序树T中是否存在key, */
/* 指针f指向T的双亲,其初始调用值为NULL */
/* 若查找成功,则指针p指向该数据元素结点,并返回TRUE */
/* 否则指针p指向查找路径上访问的最后一个结点并返回FALSE */
int SearchBST(BiTree T, int key, BiTree f, BiTree *p)
{
if (!T) /* 查找不成功 */
{
*p = f;
return 0;
}
else if (key==T->data) /* 查找成功 */
{
*p = T;
return 1;
}
else if (key<T->data)
return SearchBST(T->lchild, key, T, p); /* 在左子树中继续查找 */
else
return SearchBST(T->rchild, key, T, p); /* 在右子树中继续查找 */
}
/* 当二叉排序树T中不存在关键字等于key的数据元素时, */
int InsertBST(BiTree *T, int key)
{
BiTree p,s;
if (SearchBST(*T, key, NULL, &p) == 0) /* 查找不成功 */
{
s = (BiTree)malloc(sizeof(BiTNode));
s->data = key;
s->lchild = s->rchild = NULL;
if (!p) //如果为空树
*T = s; /* 插入s为新的根结点 */
else if (key<p->data)
p->lchild = s; /* 插入s为左孩子 */
else
p->rchild = s; /* 插入s为右孩子 */
return 1;
}
else
return 0; /* 树中已有关键字相同的结点,不再插入 */
}
/* 从二叉排序树中删除结点p,并重接它的左或右子树。 */
int Delete(BiTree *p)
{
BiTree q,s;
if((*p)->rchild==NULL) /* 右子树空则只需重接它的左子树(待删结点是叶子也走此分支) */
{
q=*p; *p=(*p)->lchild; free(q);
}
else if((*p)->lchild==NULL) /* 只需重接它的右子树 */
{
q=*p; *p=(*p)->rchild; free(q);
}
else /* 左右子树均不空 */
{
q=*p; s=(*p)->lchild;
while(s->rchild) /* 转左,然后向右到尽头(找待删结点的前驱) */
{
q=s;
s=s->rchild;
}
(*p)->data=s->data; /* s指向被删结点的直接前驱 */
if(q!=*p)
q->rchild=s->lchild; /* 重接q的右子树 */
else
q->lchild=s->lchild; /* 重接q的左子树 */
free(s);
}
return 1;
}
/* 若二叉排序树T中存在关键字等于key的数据元素时,则删除该数据元素结点, */
int DeleteBST(BiTree *T,int key)
{
if(!*T) /* 不存在关键字等于key的数据元素 */
return 0;
else
{
if (key==(*T)->data) /* 找到关键字等于key的数据元素 */
return Delete(T);
else if (key<(*T)->data)
return DeleteBST(&(*T)->lchild,key);
else
return DeleteBST(&(*T)->rchild,key);
}
}
int main()
{
int i;
int a[10]={62,88,58,47,35,73,51,99,37,93};
BiTree T=NULL;
for(i=0;i<10;i++)
{
InsertBST(&T, a[i]);
}
DeleteBST(&T,93);
DeleteBST(&T,47);
//断点跟踪查看二叉排序树结构
system("pause");
return 0;
}
平衡二叉树(AVL树)
平衡二叉树(Self-Balancing Binary SearchTree或Height-Balanced Binary Search Tree),是一种二叉排序树,其中每一个节点的左子树和右子树的高度差至多等于1。
将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF(Balance Factor)
距离插入结点最近的,且平衡因子的绝对值大于1的结点为根的子树,称为最小不平衡子树。如下图,当新插入结点37时,距离它最近的平衡因子绝对值超过1的结点是58,所以从58开始以下的子树为最小不平衡子树。
平衡二叉树构建的基本思想就是在构建二叉排序树的过程中,每当插入一个结点时,先检查是否因插入而破坏了树的平衡性,若是,则找出最小不平衡子树。在保持二叉排序树特性的前提下,调整最小不平衡子树中各结点之间的链接关系,进行相应的旋转,使之成为新的平衡子树。
/* 对以p为根的二叉排序树作右旋处理, */
/* 处理之后p指向新的树根结点,即旋转处理之前的左子树的根结点 */
void R_Rotate(BiTree *P)
{
BiTree L;
L=(*P)->lchild; /* L指向P的左子树根结点 */
(*P)->lchild=L->rchild; /* L的右子树挂接为P的左子树 */
L->rchild=(*P);
*P=L; /* P指向新的根结点 */
}
此函数代码的意思是说,当传入一个二叉排序树P,将它的左孩子结点定义为L,将L的右子树变成P的左子树,再将P改成L的右子树,最后将L替换P成为根结点。这样就完成了一次右旋操作,如图所示。图中三角形代表子树,N代表新增结点。
插入操作:
/*平衡二叉树_AVLTree*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#define MAXSIZE 100
#define LH +1 /* 左高 */
#define EH 0 /* 等高 */
#define RH -1 /* 右高 */
/* 二叉树的二叉链表结点结构定义 */
typedef struct BiTNode /* 结点结构 */
{
int data; /* 结点数据 */
int bf; /* 结点的平衡因子 */
struct BiTNode *lchild, *rchild; /* 左右孩子指针 */
} BiTNode, *BiTree;
/* 对以p为根的二叉排序树作右旋处理, */
/* 处理之后p指向新的树根结点,即旋转处理之前的左子树的根结点 */
void R_Rotate(BiTree *P)
{
BiTree L;
L=(*P)->lchild; /* L指向P的左子树根结点 */
(*P)->lchild=L->rchild; /* L的右子树挂接为P的左子树 */
L->rchild=(*P);
*P=L; /* P指向新的根结点 */
}
/* 对以P为根的二叉排序树作左旋处理, */
/* 处理之后P指向新的树根结点,即旋转处理之前的右子树的根结点0 */
void L_Rotate(BiTree *P)
{
BiTree R;
R=(*P)->rchild; /* R指向P的右子树根结点 */
(*P)->rchild=R->lchild; /* R的左子树挂接为P的右子树 */
R->lchild=(*P);
*P=R; /* P指向新的根结点 */
}
/* 对以指针T所指结点为根的二叉树作左平衡旋转处理 */
/* 本算法结束时,指针T指向新的根结点 */
void LeftBalance(BiTree *T)
{
BiTree L,Lr;
L=(*T)->lchild; /* L指向T的左子树根结点 */
switch(L->bf)
{ /* 检查T的左子树的平衡度,并作相应平衡处理 */
case LH: /* 新结点插入在T的左孩子的左子树上,要作单右旋处理 */
(*T)->bf=L->bf=EH;
R_Rotate(T);
break;
case RH: /* 新结点插入在T的左孩子的右子树上,要作双旋处理 */
Lr=L->rchild; /* Lr指向T的左孩子的右子树根 */
switch(Lr->bf)
{ /* 修改T及其左孩子的平衡因子 */
case LH: (*T)->bf=RH;
L->bf=EH;
break;
case EH: (*T)->bf=L->bf=EH;
break;
case RH: (*T)->bf=EH;
L->bf=LH;
break;
}
Lr->bf=EH;
L_Rotate(&(*T)->lchild); /* 对T的左子树作左旋平衡处理 */
R_Rotate(T); /* 对T作右旋平衡处理 */
}
}
/* 对以指针T所指结点为根的二叉树作右平衡旋转处理, */
/* 本算法结束时,指针T指向新的根结点 */
void RightBalance(BiTree *T)
{
BiTree R,Rl;
R=(*T)->rchild; /* R指向T的右子树根结点 */
switch(R->bf)
{ /* 检查T的右子树的平衡度,并作相应平衡处理 */
case RH: /* 新结点插入在T的右孩子的右子树上,要作单左旋处理 */
(*T)->bf=R->bf=EH;
L_Rotate(T);
break;
case LH: /* 新结点插入在T的右孩子的左子树上,要作双旋处理 */
Rl=R->lchild; /* Rl指向T的右孩子的左子树根 */
switch(Rl->bf)
{ /* 修改T及其右孩子的平衡因子 */
case RH: (*T)->bf=LH;
R->bf=EH;
break;
case EH: (*T)->bf=R->bf=EH;
break;
case LH: (*T)->bf=EH;
R->bf=RH;
break;
}
Rl->bf=EH;
R_Rotate(&(*T)->rchild); /* 对T的右子树作右旋平衡处理 */
L_Rotate(T); /* 对T作左旋平衡处理 */
}
}
/* 若在平衡的二叉排序树T中不存在和e有相同关键字的结点,则插入一个 */
/* 数据元素为e的新结点,并返回1,否则返回0。若因插入而使二叉排序树 */
/* 失去平衡,则作平衡旋转处理,布尔变量taller反映T长高与否。 */
int InsertAVL(BiTree *T,int e,int *taller)
{
if(!*T) //当前T为空时,则申请内存新增一个结点。
{ /* 插入新结点,树“长高”,置taller为TRUE */
*T=(BiTree)malloc(sizeof(BiTNode));
(*T)->data=e; (*T)->lchild=(*T)->rchild=NULL; (*T)->bf=EH;
*taller=1;
}
else
{
if (e==(*T)->data)
{ /* 树中已存在和e有相同关键字的结点则不再插入 */
*taller=0; return 0;
}
/*taller为1时,说明插入了结点,此时需要判断T的平衡因子,
如果是1,说明原树左子树高于右子树,插入后需要调用LeftBalance函数进行左平衡旋转处理。
如果为0或-1,则说明新插入结点没有让整棵二叉排序树失去平衡性,只需要修改相关的BF值即可。*/
if (e<(*T)->data)
{ /* 应继续在T的左子树中进行搜索 */
if(InsertAVL(&(*T)->lchild,e,taller) == 0 ) /* 未插入 */
return 0;
if(*taller) /* 已插入到T的左子树中且左子树“长高” */
switch((*T)->bf) /* 检查T的平衡度 */
{
case LH: /* 原本左子树比右子树高,需要作左平衡处理 */
LeftBalance(T); *taller=0; break;
case EH: /* 原本左、右子树等高,现因左子树增高而使树增高 */
(*T)->bf=LH; *taller=1; break;
case RH: /* 原本右子树比左子树高,现左、右子树等高 */
(*T)->bf=EH; *taller=0; break;
}
}
else
{ /* 应继续在T的右子树中进行搜索 */
if(!InsertAVL(&(*T)->rchild,e,taller)) /* 未插入 */
return 0;
if(*taller) /* 已插入到T的右子树且右子树“长高” */
switch((*T)->bf) /* 检查T的平衡度 */
{
case LH: /* 原本左子树比右子树高,现左、右子树等高 */
(*T)->bf=EH; *taller=0; break;
case EH: /* 原本左、右子树等高,现因右子树增高而使树增高 */
(*T)->bf=RH; *taller=1; break;
case RH: /* 原本右子树比左子树高,需要作右平衡处理 */
RightBalance(T); *taller=0; break;
}
}
}
return 1;
}
int main()
{
int i;
int a[10]={3,2,1,4,5,6,7,10,9,8};
BiTree T=NULL;
int taller;
for(i=0;i<10;i++)
{
InsertAVL(&T,a[i],&taller);
}
//断点跟踪查看平衡二叉树结构
system("pause");
return 0;
}
多路查找树(B树)
多路查找树(muitl-way search tree),其每一个结点的孩子数可以多于两个,且每一个结点处可以存储多个元素。由于它是查找树,所有元素之间存在某种特定的排序关系。
在这里,每一个结点可以存储多少个元素,以及它的孩子数的多少是非常关键的。
它的4种特殊形式:2-3树、2-3-4树、B树和B+树。
2-3树
2-3树:其中的每一个结点都具有两个孩子(我们称它为2结点)或三个孩子(我们称它为3结点)。
一个2结点包含一个元素和两个孩子(或没有孩子),且与二叉排序树类似,左子树包含的元素小于该元素,右子树包含的元素大于该元素。与二叉排序树不同的是,这个2结点要么没有孩子,要有就有两个,不能只有一个孩子。
一个3结点包含一小一大两个元素和三个孩子(或没有孩子),一个3结点要么没有孩子,要么具有3个孩子。如果某个3结点有孩子的话,左子树包含小于较小元素的元素,右子树包含大于较大元素的元素,中间子树包含介于两元素之间的元素。
并且2-3树中所有的叶子都在同一层次上。如下图就是一棵有效的2-3树。
2-3-4树
其实就是2-3树的概念扩展,包括了4结点的使用。一个4结点包含小中大三个元素和四个孩子(或没有孩子),一个4结点要么没有孩子,要么具有4个孩子。
B树
B树(B-tree)是一种平衡的多路查找树,2-3树和2-3-4树都是B树的特例。结点最大的孩子数目称为B树的阶(order),因此,2-3树是3阶B树,2-3-4树是4阶B树。
一个m阶的B树具有如下属性:
- 如果根结点不是叶结点,则其至少有两棵子树。
- 每一个非根的分支结点都有k-1个元素和k个孩子,其中。每一个叶子结点n都有k-1个元素,其中
1.所有叶子结点都位于同一层次。
2.所有分支结点包含下列信息数据:
(n,A0,K1,A1,K2,A2,...,Kn,An)
,其中:Ki(i=1,2,...,n)
为关键字,且Ki<Ki+1(i=1,2,...,n-1)
;Ai(i=0,2,...,n)
为指向子树根结点的指针,且指针Ai-1
所指子树中所有结点的关键字均小于Ki(i=1,2,...,n)
,An所指子树中所有结点的关键字均大于Kn,n(≤n≤m-1)
为关键字的个数(或n+1为子树的个数)。
如下图,左侧灰色方块表示当前结点的元素个数。
比方要查找数字7,首先从外存(比如硬盘中)读取得到根结点3、5、8三个元素,发现7不在当中,但在5和8之间,因此就通过A2再读取外存的6、7结点,查找到所要的元素。
外存,比如硬盘,是将所有的信息分割成相等大小的页面,每次硬盘读写的都是一个或多个完整的页面,对于一个硬盘来说,一页的长度可能是211到214个字节。
如果内存与外存交换数据次数频繁,会造成了时间效率上的瓶颈,在一个典型的B树应用中,要处理的硬盘数据量很大,因此无法一次全部装入内存。因此对B树进行调整,使得B树的阶数(或结点的元素)与硬盘存储的页面大小相匹配。比如说一棵B树的阶为1001(即1个结点包含1000个关键字),高度为2,它可以储存超过10亿个关键字,我们只要让根结点持久地保留在内存中,那么在这棵树上,寻找某一个关键字至多需要两次硬盘的读取即可。
由于B树每结点可以具有比二叉树多得多的元素,所以与二叉树的操作不同,它们减少了必须访问结点和数据块的数量,从而提高了性能。
在B树中找结点:磁盘
在结点中找关键字:内存
B+树
是在B树结构中,往返于每个结点之间意味着必须得在硬盘的页面之间进行多次访问,如下图所示,遍历这棵B树,假设每个结点都属于硬盘的不同页面,为了中序遍历所有的元素,页面2→页面1→页面3→页面1→页面4→页面1→页面5。每次经过结点遍历时,都会对结点中的元素进行一次遍历。
B+树是应文件系统所需而出的一种B树的变形树,严格意义上讲,它其实已经不是树了。在B树中,每一个元素在该树中只出现一次,有可能在叶子结点上,也有可能在分支结点上。而在B+树中,出现在分支结点中的元素会被当作它们在该分支结点位置的中序后继者(叶子结点)中再次列出。另外,每一个叶子结点都会保存一个指向后一叶子结点的指针。B+可选择多路查找或顺序查找
例如下图就是一棵B+树,灰色关键字即是根结点中的关键字在叶子结点再次列出,并且所有叶子结点都链接在一起。
如果是要随机查找,就从根结点出发,与B树的查找方式相同,只不过即使在分支结点找到了待查找的关键字,它也只是用来索引的,不能提供实际记录的访问,还是需要到达包含此关键字的终端结点。
如果是需要从最小关键字进行从小到大的顺序查找,就可以从最左侧的叶子结点出发,不经过分支结点,而是延着指向下一叶子的指针就可遍历所有的关键字。
/*B树_BTree*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#define MAXSIZE 100
#define m 3 /* B树的阶,暂设为3 */
#define N 17 /* 数据元素个数 */
typedef struct BTNode
{
int keynum; /* 结点中关键字个数,即结点的大小 */
struct BTNode *parent; /* 指向双亲结点 */
struct Node /* 结点向量类型 */
{
int key; /* 关键字向量 */
struct BTNode *ptr; /* 子树指针向量 */
int recptr; /* 记录指针向量 */
}node[m+1]; /* key,recptr的0号单元未用 */
}BTNode,*BTree; /* B树结点和B树的类型 */
typedef struct
{
BTNode *pt; /* 指向找到的结点 */
int i; /* 1..m,在结点中的关键字序号 */
int tag; /* 1:查找成功,O:查找失败 */
}Result; /* B树的查找结果类型 */
/* 在p->node[1..keynum].key中查找i,使得p->node[i].key≤K<p->node[i+1].key */
int Search(BTree p, int K)
{
int i=0,j;
for(j=1;j<=p->keynum;j++)
{
if(p->node[j].key<=K)
i=j;
}
return i;
}
/* 在m阶B树T上查找关键字K,返回结果(pt,i,tag)。若查找成功,则特征值 */
/* tag=1,指针pt所指结点中第i个关键字等于K;否则特征值tag=0,等于K的 */
/* 关键字应插入在指针Pt所指结点中第i和第i+1个关键字之间。 */
Result SearchBTree(BTree T, int K)
{
BTree p=T,q=NULL; /* 初始化,p指向待查结点,q指向p的双亲 */
int found=0;
int i=0;
Result r;
while(p&&found==0)
{
i=Search(p,K); /* p->node[i].key≤K<p->node[i+1].key */
if(i>0&&p->node[i].key==K) /* 找到待查关键字 */
found=1;
else
{
q=p;
p=p->node[i].ptr;
}
}
r.i=i;
if(found) /* 查找成功 */
{
r.pt=p;
r.tag=1;
}
else /* 查找不成功,返回K的插入位置信息 */
{
r.pt=q;
r.tag=0;
}
return r;
}
void Insert(BTree *q,int i,int key,BTree ap)
{
int j;
for(j=(*q)->keynum;j>i;j--) /* 空出(*q)->node[i+1] */
(*q)->node[j+1]=(*q)->node[j];
(*q)->node[i+1].key=key;
(*q)->node[i+1].ptr=ap;
(*q)->node[i+1].recptr=key;
(*q)->keynum++;
}
/* 将结点q分裂成两个结点,前一半保留,后一半移入新生结点ap */
void split(BTree *q,BTree *ap)
{
int i,s=(m+1)/2;
*ap=(BTree)malloc(sizeof(BTNode)); /* 生成新结点ap */
(*ap)->node[0].ptr=(*q)->node[s].ptr; /* 后一半移入ap */
for(i=s+1;i<=m;i++)
{
(*ap)->node[i-s]=(*q)->node[i];
if((*ap)->node[i-s].ptr)
(*ap)->node[i-s].ptr->parent=*ap;
}
(*ap)->keynum=m-s;
(*ap)->parent=(*q)->parent;
(*q)->keynum=s-1; /* q的前一半保留,修改keynum */
}
/* 生成含信息(T,r,ap)的新的根结点&T,原T和ap为子树指针 */
void NewRoot(BTree *T,int key,BTree ap)
{
BTree p;
p=(BTree)malloc(sizeof(BTNode));
p->node[0].ptr=*T;
*T=p;
if((*T)->node[0].ptr)
(*T)->node[0].ptr->parent=*T;
(*T)->parent=NULL;
(*T)->keynum=1;
(*T)->node[1].key=key;
(*T)->node[1].recptr=key;
(*T)->node[1].ptr=ap;
if((*T)->node[1].ptr)
(*T)->node[1].ptr->parent=*T;
}
/* 在m阶B树T上结点*q的key[i]与key[i+1]之间插入关键字K的指针r。若引起 */
/* 结点过大,则沿双亲链进行必要的结点分裂调整,使T仍是m阶B树。 */
void InsertBTree(BTree *T,int key,BTree q,int i)
{
BTree ap=NULL;
int finished=0;
int s;
int rx;
rx=key;
while(q&&!finished)
{
Insert(&q,i,rx,ap); /* 将r->key、r和ap分别插入到q->key[i+1]、q->recptr[i+1]和q->ptr[i+1]中 */
if(q->keynum<m)
finished=1; /* 插入完成 */
else
{ /* 分裂结点*q */
s=(m+1)/2;
rx=q->node[s].recptr;
split(&q,&ap); /* 将q->key[s+1..m],q->ptr[s..m]和q->recptr[s+1..m]移入新结点*ap */
q=q->parent;
if(q)
i=Search(q,key); /* 在双亲结点*q中查找rx->key的插入位置 */
}
}
if(!finished) /* T是空树(参数q初值为NULL)或根结点已分裂为结点*q和*ap */
NewRoot(T,rx,ap); /* 生成含信息(T,rx,ap)的新的根结点*T,原T和ap为子树指针 */
}
int main()
{
int r[N]={22,16,41,58,8,11,12,16,17,22,23,31,41,52,58,59,61};
BTree T=NULL;
Result s;
int i;
for(i=0;i<N;i++)
{
s=SearchBTree(T,r[i]);
if(!s.tag)
InsertBTree(&T,r[i],s.pt,s.i);
}
return 0;
}
散列表查找(哈希表)
散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key
对应一个存储位置f(key)
。
把这种对应关系f称为散列函数,又称为哈希(Hash)函数。按这个思想采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表(Hash table)。关键字对应的记录存储位置称为散列地址。
散列函数的构造方法
原则:
1.计算简单
2.散列地址分布均匀
直接定址法
f(key)=a×key+b (a、b为常数)
这样的散列函数优点就是简单、均匀,也不会产生冲突,但问题是这需要事先知道关键字的分布情况,适合查找表较小且连续的情况。由于这样的限制,在现实应用中,此方法虽然简单,但却并不常用。
数字分析法
数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀,就可以考虑用这个方法。
平方取中法
假设关键字是1234,那么它的平方就是1522756,再抽取中间的3位就是227,用做散列地址。再比如关键字是4321,那么它的平方就是18671041,抽取中间的3位就可以是671,也可以是710,用做散列地址。平方取中法比较适合于不知道关键字的分布,而位数又不是很大的情况。
除留余数法
容易发生冲突
对于散列表长为m的散列函数公式为:
f(key)=key % p (p≤m)
根据经验,若散列表表长为m,通常p为小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数。
随机数法
选择一个随机数,取关键字的随机函数值为它的散列地址。也就是f(key)=random(key)
处理散列冲突的方法
在使用散列函数后发现两个关键字key1≠key2,有f(key1)=f(key2),即有冲突
开放定址法
所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
它的公式是:
fi(key)=(f(key)+di) % m(di=1,2,3,......,m-1)
再散列函数法
对于散列表来说,事先准备多个散列函数。
fi(key)=RHi(key)(i=1,2,...,k)
这里RHi就是不同的散列函数
链地址法
将所有关键字为同义词的记录存储在一个单链表中,称这种表为同义词子表,在散列表中只存储所有同义词子表的头指针。对于关键字集合{12,67,56,16,25,37,22,29,15,47,48,34}
,用前面同样的12为除数,进行除留余数法,可得到如下结构
公共溢出区法
在查找时,对给定值通过散列函数计算出散列地址后,先与基本表的相应位置进行比对,如果相等,则查找成功;如果不相等,则到溢出表去进行顺序查找。相对于基本表而言,有冲突的数据很少的情况下,公共溢出区的结构对查找性能非常高。
/*散列表_HashTable*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#define MAXSIZE 100
#define HASHSIZE 12 /* 定义散列表长为数组的长度 */
#define NULLKEY -32768
typedef struct
{
int *elem; /* 数据元素存储基址,动态分配数组 */
int count; /* 当前数据元素个数 */
}HashTable;
int m=0; /* 散列表表长,全局变量 */
/* 初始化散列表 */
int InitHashTable(HashTable *H)
{
int i;
m=HASHSIZE;
H->count=m;
H->elem=(int *)malloc(m*sizeof(int));
for(i=0;i<m;i++)
H->elem[i]=NULLKEY;
return 1;
}
/* 散列函数 除留余数法*/
int Hash(int key)
{
return key % m;
}
/* 插入关键字进散列表 */
void InsertHash(HashTable *H,int key)
{
int addr = Hash(key); /* 求散列地址 */
while (H->elem[addr] != NULLKEY) /* 如果不为空,则冲突 */
{
addr = (addr+1) % m; /* 开放定址法的线性探测 */
}
H->elem[addr] = key; /* 直到有空位后插入关键字 */
}
/* 散列表查找关键字 */
int SearchHash(HashTable H,int key,int *addr)
{
*addr = Hash(key); /* 求散列地址 */
while(H.elem[*addr] != key) /* 如果不为空,则冲突 */
{
*addr = (*addr+1) % m; /* 开放定址法的线性探测 */
if (H.elem[*addr] == NULLKEY || *addr == Hash(key)) /* 如果循环回到原点 */
return 0; /* 则说明关键字不存在 */
}
return 1;
}
int main()
{
int arr[HASHSIZE]={12,67,56,16,25,37,22,29,15,47,48,34};
int i,p,key,result;
HashTable H;
key=39;
InitHashTable(&H);
for(i=0;i<m;i++)
InsertHash(&H,arr[i]);
result=SearchHash(H,key,&p);
if (result)
printf("查找 %d 的地址为:%d \n",key,p);
else
printf("查找 %d 失败。\n",key);
for(i=0;i<m;i++)
{
key=arr[i];
SearchHash(H,key,&p);
printf("查找 %d 的地址为:%d \n",key,p);
}
system("pause");
return 0;
}
排序
/*排序*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#define MAX_LENGTH_INSERT_SORT 7 /* 用于快速排序时判断是否选用插入排序阙值 */
#define MAXSIZE 10000
typedef struct
{
int r[MAXSIZE + 1]; /* 用于存储要排序数组,r[0]用作哨兵或临时变量 */
int length; /* 用于记录顺序表的长度 */
} SqList;
/* 交换L中数组r的下标为i和j的值 */
void swap(SqList *L, int i, int j)
{
int temp = L->r[i];
L->r[i] = L->r[j];
L->r[j] = temp;
}
void print(SqList L)
{
int i;
for (i = 1; i < L.length; i++)
printf("%d,", L.r[i]);
printf("%d", L.r[i]);
printf("\n");
}
/* 对顺序表L作交换排序(冒泡排序初级版) */
void BubbleSort0(SqList *L)
{
int i, j;
for (i = 1; i < L->length; i++)
{
for (j = i + 1; j <= L->length; j++)
{
if (L->r[i] > L->r[j])
{
swap(L, i, j);
}
}
}
}
/* 对顺序表L作冒泡排序 */
void BubbleSort(SqList *L)
{
int i, j;
for (i = 1; i < L->length; i++)
{
for (j = L->length - 1; j >= i; j--)
{
if (L->r[j] > L->r[j + 1])
{
swap(L, j, j + 1);
}
}
}
}
/* 对顺序表L作改进冒泡算法 */
void BubbleSort2(SqList *L)
{
int i, j;
int flag = 1;
for (i = 1; i < L->length && flag; i++) /* 若flag为true说明有过数据交换,否则停止循环 */
{
flag = 0;
for (j = L->length - 1; j >= i; j--)
{
if (L->r[j] > L->r[j + 1])
{
swap(L, j, j + 1);
flag = 1;
}
}
}
}
/* 对顺序表L作简单选择排序 */
void SelectSort(SqList *L)
{
int i, j, min;
for (i = 1; i < L->length; i++)
{
min = i;
for (j = i + 1; j <= L->length; j++)
{
if (L->r[min] > L->r[j])
min = j;
}
if (i != min)
swap(L, i, min);
}
}
/* 对顺序表L作直接插入排序 */
void InsertSort(SqList *L)
{
int i, j;
for (i = 2; i <= L->length; i++)
{
if (L->r[i] < L->r[i - 1])
{
L->r[0] = L->r[i]; /* 设置哨兵 */
for (j = i - 1; L->r[j] > L->r[0]; j--)
L->r[j + 1] = L->r[j];
L->r[j + 1] = L->r[0];
}
}
}
/* 对顺序表L作希尔排序 */
void ShellSort(SqList *L)
{
int i, j, k = 0;
int increment = L->length;
do
{
increment = increment / 3 + 1; /* 增量序列 */
for (i = increment + 1; i <= L->length; i++)
{
if (L->r[i] < L->r[i - increment]) /* 需将L->r[i]插入有序增量子表 */
{
L->r[0] = L->r[i]; /* 暂存在L->r[0] */
for (j = i - increment; j > 0 && L->r[0] < L->r[j]; j -= increment)
L->r[j + increment] = L->r[j]; /* 记录后移,查找插入位置 */
L->r[j + increment] = L->r[0]; /* 插入 */
}
}
printf(" 第%d趟排序结果: ", ++k);
print(*L);
} while (increment > 1);
}
/* ******************堆排序**************** */
/* 已知L->r[s..m]中记录的关键字除L->r[s]之外均满足堆的定义, */
/* 本函数调整L->r[s]的关键字,使L->r[s..m]成为一个大顶堆 */
void HeapAdjust(SqList *L, int s, int m)
{
int temp, j;
temp = L->r[s];
for (j = 2 * s; j <= m; j *= 2) /* 沿关键字较大的孩子结点向下筛选 */
{
if (j < m && L->r[j] < L->r[j + 1])
++j; /* j为关键字中较大的记录的下标 */
if (temp >= L->r[j])
break; /* rc应插入在位置s上 */
L->r[s] = L->r[j];
s = j;
}
L->r[s] = temp; /* 插入 */
}
/* 对顺序表L进行堆排序 */
void HeapSort(SqList *L)
{
int i;
for (i = L->length / 2; i > 0; i--) /* 把L中的r构建成一个大根堆 */
HeapAdjust(L, i, L->length);
for (i = L->length; i > 1; i--)
{
swap(L, 1, i); /* 将堆顶记录和当前未经排序子序列的最后一个记录交换 */
HeapAdjust(L, 1, i - 1); /* 将L->r[1..i-1]重新调整为大根堆 */
}
}
/* **************************************** */
/* ****************归并排序****************** */
/* 将有序的SR[i..m]和SR[m+1..n]归并为有序的TR[i..n] */
void Merge(int SR[], int TR[], int i, int m, int n)
{
int j, k, l;
for (j = m + 1, k = i; i <= m && j <= n; k++) /* 将SR中记录由小到大地并入TR */
{
if (SR[i] < SR[j])
TR[k] = SR[i++];
else
TR[k] = SR[j++];
}
if (i <= m)
{
for (l = 0; l <= m - i; l++)
TR[k + l] = SR[i + l]; /* 将剩余的SR[i..m]复制到TR */
}
if (j <= n)
{
for (l = 0; l <= n - j; l++)
TR[k + l] = SR[j + l]; /* 将剩余的SR[j..n]复制到TR */
}
}
/* 递归法 */
/* 将SR[s..t]归并排序为TR1[s..t] */
void MSort(int SR[], int TR1[], int s, int t)
{
int m;
int TR2[MAXSIZE + 1];
if (s == t)
TR1[s] = SR[s];
else
{
m = (s + t) / 2; /* 将SR[s..t]平分为SR[s..m]和SR[m+1..t] */
MSort(SR, TR2, s, m); /* 递归地将SR[s..m]归并为有序的TR2[s..m] */
MSort(SR, TR2, m + 1, t); /* 递归地将SR[m+1..t]归并为有序的TR2[m+1..t] */
Merge(TR2, TR1, s, m, t); /* 将TR2[s..m]和TR2[m+1..t]归并到TR1[s..t] */
}
}
/* 对顺序表L作归并排序 */
void MergeSort(SqList *L)
{
MSort(L->r, L->r, 1, L->length);
}
/* 非递归法 */
/* 将SR[]中相邻长度为s的子序列两两归并到TR[] */
void MergePass(int SR[], int TR[], int s, int n)
{
int i = 1;
int j;
while (i <= n - 2 * s + 1)
{ /* 两两归并 */
Merge(SR, TR, i, i + s - 1, i + 2 * s - 1);
i = i + 2 * s;
}
if (i < n - s + 1) /* 归并最后两个序列 */
Merge(SR, TR, i, i + s - 1, n);
else /* 若最后只剩下单个子序列 */
for (j = i; j <= n; j++)
TR[j] = SR[j];
}
/* 对顺序表L作归并非递归排序 */
void MergeSort2(SqList *L)
{
int *TR = (int *)malloc(L->length * sizeof(int)); /* 申请额外空间 */
int k = 1;
while (k < L->length)
{
MergePass(L->r, TR, k, L->length);
k = 2 * k; /* 子序列长度加倍 */
MergePass(TR, L->r, k, L->length);
k = 2 * k; /* 子序列长度加倍 */
}
}
/* **************************************** */
/* ***************快速排序***************** */
/* 交换顺序表L中子表的记录,使枢轴记录到位,并返回其所在位置 */
/* 此时在它之前(后)的记录均不大(小)于它。 */
int Partition(SqList *L, int low, int high)
{
int pivotkey;
pivotkey = L->r[low]; /* 用子表的第一个记录作枢轴记录 */
while (low < high) /* 从表的两端交替地向中间扫描 */
{
while (low < high && L->r[high] >= pivotkey)
high--;
swap(L, low, high); /* 将比枢轴记录小的记录交换到低端 */
while (low < high && L->r[low] <= pivotkey)
low++;
swap(L, low, high); /* 将比枢轴记录大的记录交换到高端 */
}
return low; /* 返回枢轴所在位置 */
}
/* 对顺序表L中的子序列L->r[low..high]作快速排序 */
void QSort(SqList *L, int low, int high)
{
int pivot;
if (low < high)
{
pivot = Partition(L, low, high); /* 将L->r[low..high]一分为二,算出枢轴值pivot */
QSort(L, low, pivot - 1); /* 对低子表递归排序 */
QSort(L, pivot + 1, high); /* 对高子表递归排序 */
}
}
/* 对顺序表L作快速排序 */
void QuickSort(SqList *L)
{
QSort(L, 1, L->length);
}
/* **************************************** */
/* ***************改进后快速排序***************** */
/* 快速排序优化算法 */
int Partition1(SqList *L, int low, int high)
{
int pivotkey;
int m = low + (high - low) / 2; /* 计算数组中间的元素的下标 */
if (L->r[low] > L->r[high])
swap(L, low, high); /* 交换左端与右端数据,保证左端较小 */
if (L->r[m] > L->r[high])
swap(L, high, m); /* 交换中间与右端数据,保证中间较小 */
if (L->r[m] > L->r[low])
swap(L, m, low); /* 交换中间与左端数据,保证左端较小 */
pivotkey = L->r[low]; /* 用子表的第一个记录作枢轴记录 */
L->r[0] = pivotkey; /* 将枢轴关键字备份到L->r[0] */
while (low < high) /* 从表的两端交替地向中间扫描 */
{
while (low < high && L->r[high] >= pivotkey)
high--;
L->r[low] = L->r[high];
while (low < high && L->r[low] <= pivotkey)
low++;
L->r[high] = L->r[low];
}
L->r[low] = L->r[0];
return low; /* 返回枢轴所在位置 */
}
void QSort1(SqList *L, int low, int high)
{
int pivot;
if ((high - low) > MAX_LENGTH_INSERT_SORT)
{
while (low < high)
{
pivot = Partition1(L, low, high); /* 将L->r[low..high]一分为二,算出枢轴值pivot */
QSort1(L, low, pivot - 1); /* 对低子表递归排序 */
/* QSort(L,pivot+1,high); /* 对高子表递归排序 */
low = pivot + 1; /* 尾递归 */
}
}
else
InsertSort(L);
}
/* 对顺序表L作快速排序 */
void QuickSort1(SqList *L)
{
QSort1(L, 1, L->length);
}
/* **************************************** */
#define N 9
int main()
{
int i;
int d[N] = {50, 10, 90, 30, 70, 40, 80, 60, 20};
SqList l0, l1, l2, l3, l4, l5, l6, l7, l8, l9, l10;
for (i = 0; i < N; i++)
l0.r[i + 1] = d[i];
l0.length = N;
l1 = l2 = l3 = l4 = l5 = l6 = l7 = l8 = l9 = l10 = l0;
printf("排序前:\n"); print(l0);
printf("初级冒泡排序:\n"); BubbleSort0(&l0); print(l0);
printf("冒泡排序:\n"); BubbleSort(&l1); print(l1);
printf("改进冒泡排序:\n"); BubbleSort2(&l2); print(l2);
printf("选择排序:\n"); SelectSort(&l3); print(l3);
printf("直接插入排序:\n"); InsertSort(&l4); print(l4);
printf("希尔排序:\n"); ShellSort(&l5); print(l5);
printf("堆排序:\n"); HeapSort(&l6); print(l6);
printf("归并排序(递归):\n"); MergeSort(&l7); print(l7);
printf("归并排序(非递归):\n"); MergeSort2(&l8); print(l8);
printf("快速排序:\n"); QuickSort(&l9); print(l9);
printf("改进快速排序:\n"); QuickSort1(&l10); print(l10);
system("pause");
return 0;
}